Radish alpha
h
rad:z3gqcJUoA1n9HaHKufZs5FCSGazv5
Radicle Heartwood Protocol & Stack
Radicle
Git
Refactor signing and public key usage
Merged fintohaps opened 1 year ago

This patch aims to refactor some of our usage of PublicKey and Signer. The

motivation for this is to help make way for thinking about agent repositories – and, in the future, multi-device.

The main motivation is to help gain some separation when we want to think about a node (or device) signing artifacts, and when will want to think about an agent signing artifacts.

The reason for moving away from radicle_crypto::Signer and towards signature::Signer is because the former ties itself very closely to the PublicKey. In the future, we will want signers that are not necessarily a single PublicKey – but may come from many device, for example.

However, there are places where we do want this association, and that is why the Device type is introduced.

Furthermore, we also want to try help separate when a PublicKey is required for verifying and when it used for namespacing in the Git repository. For now, this separation is only done by abstracting away the namespacing for the COB storage.

See the commits for more details on the changes.

67 files changed +1030 -505 41f9048d 9988b63b
modified Cargo.lock
@@ -2284,6 +2284,7 @@ dependencies = [
 "radicle-git-ext",
 "serde",
 "serde_json",
+
 "signature 2.2.0",
 "tempfile",
 "thiserror 1.0.69",
]
@@ -2316,6 +2317,7 @@ dependencies = [
 "radicle-git-ext",
 "radicle-ssh",
 "serde",
+
 "signature 2.2.0",
 "sqlite",
 "ssh-key",
 "tempfile",
modified radicle-cli/src/commands/cob.rs
@@ -471,7 +471,7 @@ pub fn run(Options { op }: Options, ctx: impl term::Context) -> anyhow::Result<(
                    let actions: Vec<cob::patch::Action> = read_jsonl(reader)?;
                    let mut patches = profile.patches_mut(&repo)?;
                    let mut patch = patches.get_mut(oid)?;
-
                    patch.transaction(&message, &profile.signer()?, |tx| {
+
                    patch.transaction(&message, &*profile.signer()?, |tx| {
                        tx.extend(actions)?;
                        tx.embed(embeds)?;
                        Ok(())
@@ -481,7 +481,7 @@ pub fn run(Options { op }: Options, ctx: impl term::Context) -> anyhow::Result<(
                    let actions: Vec<cob::issue::Action> = read_jsonl(reader)?;
                    let mut issues = profile.issues_mut(&repo)?;
                    let mut issue = issues.get_mut(oid)?;
-
                    issue.transaction(&message, &profile.signer()?, |tx| {
+
                    issue.transaction(&message, &*profile.signer()?, |tx| {
                        tx.extend(actions)?;
                        tx.embed(embeds)?;
                        Ok(())
modified radicle-cli/src/commands/id.rs
@@ -6,10 +6,12 @@ use anyhow::{anyhow, Context};

use radicle::cob::identity::{self, IdentityMut, Revision, RevisionId};
use radicle::identity::{doc, Doc, Identity, PayloadError, RawDoc, Visibility};
-
use radicle::prelude::{Did, RepoId, Signer};
+
use radicle::node::device::Device;
+
use radicle::node::NodeId;
+
use radicle::prelude::{Did, RepoId};
use radicle::storage::refs;
use radicle::storage::{ReadRepository, ReadStorage as _, WriteRepository};
-
use radicle::{cob, Profile};
+
use radicle::{cob, crypto, Profile};
use radicle_surf::diff::Diff;
use radicle_term::Element;
use serde_json as json;
@@ -722,13 +724,17 @@ and description.
    Ok(result)
}

-
fn update<R: WriteRepository + cob::Store, G: Signer>(
+
fn update<R, G>(
    title: Option<String>,
    description: Option<String>,
    doc: Doc,
    current: &mut IdentityMut<R>,
-
    signer: &G,
-
) -> anyhow::Result<Revision> {
+
    signer: &Device<G>,
+
) -> anyhow::Result<Revision>
+
where
+
    R: WriteRepository + cob::Store<Namespace = NodeId>,
+
    G: crypto::signature::Signer<crypto::Signature>,
+
{
    if let Some((title, description)) = edit_title_description(title, description)? {
        let id = current.update(title, description, &doc, signer)?;
        let revision = current
modified radicle-cli/src/commands/inbox.rs
@@ -12,7 +12,7 @@ use radicle::issue::cache::Issues as _;
use radicle::node::notifications;
use radicle::node::notifications::*;
use radicle::patch::cache::Patches as _;
-
use radicle::prelude::{Profile, RepoId};
+
use radicle::prelude::{NodeId, Profile, RepoId};
use radicle::storage::{BranchName, ReadRepository, ReadStorage};
use radicle::{cob, git, Storage};

@@ -261,7 +261,7 @@ fn list_repo<'a, R: ReadStorage>(
    profile: &Profile,
) -> anyhow::Result<Option<term::VStack<'a>>>
where
-
    <R as ReadStorage>::Repository: cob::Store,
+
    <R as ReadStorage>::Repository: cob::Store<Namespace = NodeId>,
{
    let mut table = term::Table::new(term::TableOptions {
        spacing: 3,
modified radicle-cli/src/commands/issue.rs
@@ -10,8 +10,10 @@ use anyhow::{anyhow, Context as _};
use radicle::cob::common::{Label, Reaction};
use radicle::cob::issue::{CloseReason, State};
use radicle::cob::{issue, thread};
-
use radicle::crypto::Signer;
+
use radicle::crypto;
use radicle::issue::cache::Issues as _;
+
use radicle::node::device::Device;
+
use radicle::node::NodeId;
use radicle::prelude::{Did, RepoId};
use radicle::profile;
use radicle::storage;
@@ -810,12 +812,12 @@ fn open<R, G>(
    assignees: Vec<Did>,
    options: &Options,
    cache: &mut issue::Cache<issue::Issues<'_, R>, cob::cache::StoreWriter>,
-
    signer: &G,
+
    signer: &Device<G>,
    profile: &Profile,
) -> anyhow::Result<()>
where
-
    R: ReadRepository + WriteRepository + cob::Store,
-
    G: Signer,
+
    R: ReadRepository + WriteRepository + cob::Store<Namespace = NodeId>,
+
    G: crypto::signature::Signer<crypto::Signature>,
{
    let (title, description) = if let (Some(t), Some(d)) = (title.as_ref(), description.as_ref()) {
        (t.to_owned(), d.to_owned())
@@ -845,11 +847,11 @@ fn edit<'a, 'g, R, G>(
    id: Rev,
    title: Option<String>,
    description: Option<String>,
-
    signer: &G,
+
    signer: &Device<G>,
) -> anyhow::Result<issue::IssueMut<'a, 'g, R, cob::cache::StoreWriter>>
where
-
    R: WriteRepository + ReadRepository + cob::Store,
-
    G: radicle::crypto::Signer,
+
    R: WriteRepository + ReadRepository + cob::Store<Namespace = NodeId>,
+
    G: crypto::signature::Signer<crypto::Signature>,
{
    let id = id.resolve(&repo.backend)?;
    let mut issue = issues.get_mut(&id)?;
@@ -890,7 +892,7 @@ where
}

/// Get a comment from the user, by prompting.
-
pub fn prompt_comment<R: WriteRepository + radicle::cob::Store>(
+
pub fn prompt_comment<R: WriteRepository + radicle::cob::Store<Namespace = NodeId>>(
    message: Message,
    reply_to: Option<Rev>,
    issue: &issue::Issue,
modified radicle-cli/src/commands/job.rs
@@ -4,8 +4,9 @@ use std::ffi::OsString;
use anyhow::{anyhow, Context as _};

use radicle::cob::job::{JobStore, Reason, State};
-
use radicle::crypto::Signer;
-
use radicle::node::Handle;
+
use radicle::crypto;
+
use radicle::node::device::Device;
+
use radicle::node::{Handle, NodeId};
use radicle::storage::{WriteRepository, WriteStorage};
use radicle::{cob, Node};

@@ -260,13 +261,17 @@ pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> {
    Ok(())
}

-
fn trigger<R: WriteRepository + cob::Store, G: Signer>(
+
fn trigger<R, G>(
    commit: &Rev,
    store: &mut JobStore<R>,
    repo: &radicle::storage::git::Repository,
-
    signer: &G,
+
    signer: &Device<G>,
    quiet: bool,
-
) -> anyhow::Result<()> {
+
) -> anyhow::Result<()>
+
where
+
    R: WriteRepository + cob::Store<Namespace = NodeId>,
+
    G: crypto::signature::Signer<crypto::Signature>,
+
{
    let commit = commit.resolve(&repo.backend)?;
    let job = store.create(commit, signer)?;
    if !quiet {
@@ -275,14 +280,18 @@ fn trigger<R: WriteRepository + cob::Store, G: Signer>(
    Ok(())
}

-
fn start<R: WriteRepository + cob::Store, G: Signer>(
+
fn start<R, G>(
    job_id: &Rev,
    run_id: &str,
    info_url: Option<String>,
    store: &mut JobStore<R>,
    repo: &radicle::storage::git::Repository,
-
    signer: &G,
-
) -> anyhow::Result<()> {
+
    signer: &Device<G>,
+
) -> anyhow::Result<()>
+
where
+
    R: WriteRepository + cob::Store<Namespace = NodeId>,
+
    G: crypto::signature::Signer<crypto::Signature>,
+
{
    let job_id = job_id.resolve(&repo.backend)?;
    let mut job = store.get_mut(&job_id)?;

@@ -292,7 +301,9 @@ fn start<R: WriteRepository + cob::Store, G: Signer>(
}

// TODO: This should use the COB cache for performance.
-
fn list<R: WriteRepository + cob::Store>(store: &JobStore<R>) -> anyhow::Result<()> {
+
fn list<R: WriteRepository + cob::Store<Namespace = NodeId>>(
+
    store: &JobStore<R>,
+
) -> anyhow::Result<()> {
    if store.is_empty()? {
        term::print(term::format::italic("Nothing to show."));
        return Ok(());
@@ -334,7 +345,7 @@ fn list<R: WriteRepository + cob::Store>(store: &JobStore<R>) -> anyhow::Result<
    Ok(())
}

-
fn show<R: WriteRepository + cob::Store>(
+
fn show<R: WriteRepository + cob::Store<Namespace = NodeId>>(
    job_id: &Rev,
    store: &JobStore<R>,
    repo: &radicle::storage::git::Repository,
@@ -349,13 +360,17 @@ fn show<R: WriteRepository + cob::Store>(
    Ok(())
}

-
fn finish<R: WriteRepository + cob::Store, G: Signer>(
+
fn finish<R, G>(
    job_id: &Rev,
    reason: Reason,
    store: &mut JobStore<R>,
    repo: &radicle::storage::git::Repository,
-
    signer: &G,
-
) -> anyhow::Result<()> {
+
    signer: &Device<G>,
+
) -> anyhow::Result<()>
+
where
+
    R: WriteRepository + cob::Store<Namespace = NodeId>,
+
    G: crypto::signature::Signer<crypto::Signature>,
+
{
    let job_id = job_id.resolve(&repo.backend)?;
    let mut job = store.get_mut(&job_id)?;

modified radicle-cli/src/commands/patch/edit.rs
@@ -3,6 +3,7 @@ use super::*;
use radicle::cob;
use radicle::cob::patch;
use radicle::crypto;
+
use radicle::node::device::Device;
use radicle::prelude::*;
use radicle::storage::git::Repository;

@@ -32,10 +33,10 @@ fn edit_root<G>(
    mut patch: patch::PatchMut<'_, '_, Repository, cob::cache::StoreWriter>,
    title: String,
    description: String,
-
    signer: &G,
+
    signer: &Device<G>,
) -> anyhow::Result<()>
where
-
    G: crypto::Signer,
+
    G: crypto::signature::Signer<crypto::Signature>,
{
    let title = if title != patch.title() {
        Some(title)
@@ -75,10 +76,10 @@ fn edit_revision<G>(
    revision: patch::RevisionId,
    mut title: String,
    description: String,
-
    signer: &G,
+
    signer: &Device<G>,
) -> anyhow::Result<()>
where
-
    G: crypto::Signer,
+
    G: crypto::signature::Signer<crypto::Signature>,
{
    let embeds = patch.embeds().to_owned();
    let description = if description.is_empty() {
modified radicle-cli/src/commands/patch/review.rs
@@ -89,10 +89,10 @@ pub fn run(
                .minimal(true)
                .context_lines(unified as u32);

-
            builder::ReviewBuilder::new(patch_id, signer, repository)
+
            builder::ReviewBuilder::new(patch_id, repository)
                .hunk(hunk)
                .verdict(verdict)
-
                .run(revision, &mut opts)?;
+
                .run(revision, &mut opts, &signer)?;
        }
        Operation::Review { verdict, .. } => {
            let message = options.message.get(REVIEW_HELP_MSG)?;
modified radicle-cli/src/commands/patch/review/builder.rs
@@ -21,7 +21,9 @@ use std::{fmt, io};
use radicle::cob;
use radicle::cob::patch::{PatchId, Revision, Verdict};
use radicle::cob::{CodeLocation, CodeRange};
+
use radicle::crypto;
use radicle::git;
+
use radicle::node::device::Device;
use radicle::prelude::*;
use radicle::storage::git::{cob::DraftStore, Repository};
use radicle_git_ext::Oid;
@@ -569,11 +571,9 @@ impl<'a> Brain<'a> {
}

/// Builds a patch review interactively, across multiple files.
-
pub struct ReviewBuilder<'a, G> {
+
pub struct ReviewBuilder<'a> {
    /// Patch being reviewed.
    patch_id: PatchId,
-
    /// Signer.
-
    signer: G,
    /// Stored copy of repository.
    repo: &'a Repository,
    /// Single hunk review.
@@ -582,12 +582,11 @@ pub struct ReviewBuilder<'a, G> {
    verdict: Option<Verdict>,
}

-
impl<'a, G: Signer> ReviewBuilder<'a, G> {
+
impl<'a> ReviewBuilder<'a> {
    /// Create a new review builder.
-
    pub fn new(patch_id: PatchId, signer: G, repo: &'a Repository) -> Self {
+
    pub fn new(patch_id: PatchId, repo: &'a Repository) -> Self {
        Self {
            patch_id,
-
            signer,
            repo,
            hunk: None,
            verdict: None,
@@ -607,9 +606,16 @@ impl<'a, G: Signer> ReviewBuilder<'a, G> {
    }

    /// Run the review builder for the given revision.
-
    pub fn run(self, revision: &Revision, opts: &mut git::raw::DiffOptions) -> anyhow::Result<()> {
+
    pub fn run<G>(
+
        self,
+
        revision: &Revision,
+
        opts: &mut git::raw::DiffOptions,
+
        signer: &Device<G>,
+
    ) -> anyhow::Result<()>
+
    where
+
        G: crypto::signature::Signer<crypto::Signature>,
+
    {
        let repo = self.repo.raw();
-
        let signer = &self.signer;
        let base = repo.find_commit((*revision.base()).into())?;
        let patch_id = self.patch_id;
        let tree = {
modified radicle-cli/src/terminal/cob.rs
@@ -3,6 +3,7 @@ use radicle::{
        self,
        cache::{MigrateCallback, MigrateProgress},
    },
+
    prelude::NodeId,
    profile,
    storage::ReadRepository,
    Profile,
@@ -61,7 +62,7 @@ pub fn patches<'a, R>(
    repository: &'a R,
) -> Result<cob::patch::Cache<cob::patch::Patches<'a, R>, cob::cache::StoreReader>, anyhow::Error>
where
-
    R: ReadRepository + cob::Store,
+
    R: ReadRepository + cob::Store<Namespace = NodeId>,
{
    profile.patches(repository).map_err(with_hint)
}
@@ -72,7 +73,7 @@ pub fn patches_mut<'a, R>(
    repository: &'a R,
) -> Result<cob::patch::Cache<cob::patch::Patches<'a, R>, cob::cache::StoreWriter>, anyhow::Error>
where
-
    R: ReadRepository + cob::Store,
+
    R: ReadRepository + cob::Store<Namespace = NodeId>,
{
    profile.patches_mut(repository).map_err(with_hint)
}
@@ -83,7 +84,7 @@ pub fn issues<'a, R>(
    repository: &'a R,
) -> Result<cob::issue::Cache<cob::issue::Issues<'a, R>, cob::cache::StoreReader>, anyhow::Error>
where
-
    R: ReadRepository + cob::Store,
+
    R: ReadRepository + cob::Store<Namespace = NodeId>,
{
    profile.issues(repository).map_err(with_hint)
}
@@ -94,7 +95,7 @@ pub fn issues_mut<'a, R>(
    repository: &'a R,
) -> Result<cob::issue::Cache<cob::issue::Issues<'a, R>, cob::cache::StoreWriter>, anyhow::Error>
where
-
    R: ReadRepository + cob::Store,
+
    R: ReadRepository + cob::Store<Namespace = NodeId>,
{
    profile.issues_mut(repository).map_err(with_hint)
}
modified radicle-cli/src/terminal/io.rs
@@ -3,7 +3,8 @@ use radicle::cob::issue::Issue;
use radicle::cob::thread::{Comment, CommentId};
use radicle::cob::Reaction;
use radicle::crypto::ssh::keystore::MemorySigner;
-
use radicle::crypto::{ssh::Keystore, Signer};
+
use radicle::crypto::ssh::Keystore;
+
use radicle::node::device::{BoxedDevice, Device};
use radicle::profile::env::RAD_PASSPHRASE;
use radicle::profile::Profile;

@@ -43,7 +44,7 @@ impl inquire::validator::StringValidator for PassphraseValidator {

/// Get the signer. First we try getting it from ssh-agent, otherwise we prompt the user,
/// if we're connected to a TTY.
-
pub fn signer(profile: &Profile) -> anyhow::Result<Box<dyn Signer>> {
+
pub fn signer(profile: &Profile) -> anyhow::Result<BoxedDevice> {
    if let Ok(signer) = profile.signer() {
        return Ok(signer);
    }
@@ -62,7 +63,7 @@ pub fn signer(profile: &Profile) -> anyhow::Result<Box<dyn Signer>> {

    spinner.finish();

-
    Ok(signer.boxed())
+
    Ok(Device::from(signer).boxed())
}

pub fn comment_select(issue: &Issue) -> anyhow::Result<(&CommentId, &Comment)> {
modified radicle-cob/Cargo.toml
@@ -25,6 +25,7 @@ nonempty = { version = "0.9.0", features = ["serialize"] }
once_cell = { version = "1.13" }
radicle-git-ext = { version = "0.8.0", features = ["serde"] }
serde_json = { version = "1.0" }
+
signature = { version = "2.2" }
thiserror = { version = "1.0" }

[dependencies.git2]
@@ -54,4 +55,4 @@ qcheck-macros = { version = "1", default-features = false }
[dev-dependencies.radicle-crypto]
path = "../radicle-crypto"
version = "0"
-
features = ["test"]
+
features = ["test", "radicle-git-ext"]
modified radicle-cob/src/backend/git/change.rs
@@ -103,7 +103,7 @@ impl change::Storage for git2::Repository {
        spec: store::Template<Self::ObjectId>,
    ) -> Result<Entry, Self::StoreError>
    where
-
        Signer: crypto::Signer,
+
        Signer: signature::Signer<Self::Signatures>,
    {
        let change::Template {
            type_name,
@@ -115,11 +115,7 @@ impl change::Storage for git2::Repository {
        let manifest = store::Manifest::new(type_name, Version::default());
        let revision = write_manifest(self, &manifest, embeds, &contents)?;
        let tree = self.find_tree(revision)?;
-
        let signature = {
-
            let sig = signer.sign(revision.as_bytes());
-
            let key = signer.public_key();
-
            ExtendedSignature::new(*key, sig)
-
        };
+
        let signature = signer.sign(revision.as_bytes());

        // Make sure there are no duplicates in the related list.
        related.sort();
modified radicle-cob/src/change/store.rs
@@ -27,7 +27,7 @@ pub trait Storage {
        template: Template<Self::ObjectId>,
    ) -> Result<Entry<Self::Parent, Self::ObjectId, Self::Signatures>, Self::StoreError>
    where
-
        G: crypto::Signer;
+
        G: signature::Signer<Self::Signatures>;

    /// Load a change entry.
    #[allow(clippy::type_complexity)]
modified radicle-cob/src/object/collaboration/create.rs
@@ -1,7 +1,6 @@
// Copyright © 2022 The Radicle Link Contributors

use nonempty::NonEmpty;
-
use radicle_crypto::PublicKey;

use crate::Embed;
use crate::Evaluate;
@@ -58,13 +57,13 @@ pub fn create<T, S, G>(
    signer: &G,
    resource: Option<Oid>,
    related: Vec<Oid>,
-
    identifier: &PublicKey,
+
    identifier: &S::Namespace,
    args: Create,
) -> Result<CollaborativeObject<T>, error::Create>
where
    T: Evaluate<S>,
    S: Store,
-
    G: crypto::Signer,
+
    G: signature::Signer<crate::ExtendedSignature>,
{
    let type_name = args.type_name.clone();
    let version = args.version;
modified radicle-cob/src/object/collaboration/remove.rs
@@ -1,7 +1,5 @@
// Copyright © 2022 The Radicle Link Contributors

-
use radicle_crypto::PublicKey;
-

use crate::{ObjectId, Store, TypeName};

use super::error;
@@ -17,7 +15,7 @@ use super::error;
/// type.
pub fn remove<S>(
    storage: &S,
-
    identifier: &PublicKey,
+
    identifier: &S::Namespace,
    typename: &TypeName,
    oid: &ObjectId,
) -> Result<(), error::Remove>
modified radicle-cob/src/object/collaboration/update.rs
@@ -3,11 +3,10 @@ use std::iter;

use git_ext::Oid;
use nonempty::NonEmpty;
-
use radicle_crypto::PublicKey;

use crate::{
    change, change_graph::ChangeGraph, history::EntryId, CollaborativeObject, Embed, Evaluate,
-
    ObjectId, Store, TypeName,
+
    ExtendedSignature, ObjectId, Store, TypeName,
};

use super::error;
@@ -61,13 +60,13 @@ pub fn update<T, S, G>(
    signer: &G,
    resource: Option<Oid>,
    related: Vec<Oid>,
-
    identifier: &PublicKey,
+
    identifier: &S::Namespace,
    args: Update,
) -> Result<Updated<T>, error::Update>
where
    T: Evaluate<S>,
    S: Store,
-
    G: crypto::Signer,
+
    G: signature::Signer<ExtendedSignature>,
{
    let Update {
        type_name: ref typename,
modified radicle-cob/src/object/storage.rs
@@ -4,7 +4,6 @@ use std::{collections::BTreeMap, error::Error};

use git_ext::ref_format::RefString;
use git_ext::Oid;
-
use radicle_crypto::PublicKey;

use crate::change::EntryId;
use crate::{ObjectId, TypeName};
@@ -59,6 +58,8 @@ pub trait Storage {
    type UpdateError: Error + Send + Sync + 'static;
    type RemoveError: Error + Send + Sync + 'static;

+
    type Namespace;
+

    /// Get all references which point to a head of the change graph for a
    /// particular object
    fn objects(
@@ -74,7 +75,7 @@ pub trait Storage {
    /// Update a ref to a particular collaborative object
    fn update(
        &self,
-
        identifier: &PublicKey,
+
        namespace: &Self::Namespace,
        typename: &TypeName,
        object_id: &ObjectId,
        entry: &EntryId,
@@ -83,7 +84,7 @@ pub trait Storage {
    /// Remove a ref to a particular collaborative object
    fn remove(
        &self,
-
        identifier: &PublicKey,
+
        namespace: &Self::Namespace,
        typename: &TypeName,
        object_id: &ObjectId,
    ) -> Result<(), Self::RemoveError>;
modified radicle-cob/src/test/storage.rs
@@ -1,12 +1,12 @@
use std::{collections::BTreeMap, convert::TryFrom as _};

-
use radicle_crypto::PublicKey;
+
use radicle_git_ext::ref_format::{refname, Component};
use tempfile::TempDir;

use crate::{
    change,
    object::{self, Reference},
-
    ObjectId, Store,
+
    signatures, ObjectId, Store,
};

pub mod error {
@@ -76,7 +76,7 @@ impl change::Storage for Storage {
        Self::StoreError,
    >
    where
-
        Signer: crypto::Signer,
+
        Signer: signature::Signer<signatures::ExtendedSignature>,
    {
        self.as_raw().store(authority, parents, signer, spec)
    }
@@ -105,6 +105,8 @@ impl object::Storage for Storage {
    type UpdateError = git2::Error;
    type RemoveError = git2::Error;

+
    type Namespace = crypto::PublicKey;
+

    fn objects(
        &self,
        typename: &crate::TypeName,
@@ -148,12 +150,16 @@ impl object::Storage for Storage {

    fn update(
        &self,
-
        identifier: &PublicKey,
+
        namespace: &Self::Namespace,
        typename: &crate::TypeName,
        object_id: &ObjectId,
        entry: &change::EntryId,
    ) -> Result<(), Self::UpdateError> {
-
        let name = format!("refs/rad/{}/cobs/{}/{}", identifier, typename, object_id);
+
        let name = refname!("refs/rad")
+
            .join(Component::from(namespace))
+
            .join(refname!("cobs"))
+
            .join::<Component>(typename.into())
+
            .join::<Component>(object_id.into());
        self.raw
            .reference(&name, (*entry).into(), true, "new change")?;
        Ok(())
@@ -161,11 +167,15 @@ impl object::Storage for Storage {

    fn remove(
        &self,
-
        identifier: &PublicKey,
+
        namespace: &Self::Namespace,
        typename: &crate::TypeName,
        object_id: &ObjectId,
    ) -> Result<(), Self::RemoveError> {
-
        let name = format!("refs/rad/{}/cobs/{}/{}", identifier, typename, object_id);
+
        let name = refname!("refs/rad")
+
            .join(Component::from(namespace))
+
            .join(refname!("cobs"))
+
            .join::<Component>(typename.into())
+
            .join::<Component>(object_id.into());
        self.raw.find_reference(&name)?.delete()?;

        Ok(())
modified radicle-crypto/Cargo.toml
@@ -23,6 +23,7 @@ fastrand = { version = "2.0.0", default-features = false, optional = true }
multibase = { version = "0.9.1" }
ec25519 = { version = "0.1.0", features = [] }
serde = { version = "1", features = ["derive"] }
+
signature = { version = "2.2"  }
sqlite = { version = "0.32.0", optional = true, features = ["bundled"] }
thiserror = { version = "1" }
zeroize = { version = "1.5.7" }
modified radicle-crypto/src/lib.rs
@@ -8,6 +8,8 @@ use thiserror::Error;

pub use ed25519::{edwards25519, Error, KeyPair, Seed};

+
pub extern crate signature;
+

#[cfg(feature = "ssh")]
pub mod ssh;
#[cfg(any(test, feature = "test"))]
modified radicle-crypto/src/ssh.rs
@@ -32,6 +32,12 @@ pub struct ExtendedSignature {
    pub sig: crypto::Signature,
}

+
impl From<ExtendedSignature> for crypto::Signature {
+
    fn from(ExtendedSignature { sig, .. }: ExtendedSignature) -> Self {
+
        sig
+
    }
+
}
+

impl ExtendedSignature {
    /// Create a new extended signature.
    pub fn new(public_key: crypto::PublicKey, signature: crypto::Signature) -> Self {
modified radicle-crypto/src/ssh/agent.rs
@@ -11,6 +11,8 @@ pub use std::net::TcpStream as Stream;
#[cfg(unix)]
pub use std::os::unix::net::UnixStream as Stream;

+
use super::ExtendedSignature;
+

pub struct Agent {
    client: AgentClient<Stream>,
}
@@ -58,6 +60,21 @@ pub struct AgentSigner {
    public: PublicKey,
}

+
impl signature::Signer<Signature> for AgentSigner {
+
    fn try_sign(&self, msg: &[u8]) -> Result<Signature, signature::Error> {
+
        Signer::try_sign(self, msg).map_err(signature::Error::from_source)
+
    }
+
}
+

+
impl signature::Signer<ExtendedSignature> for AgentSigner {
+
    fn try_sign(&self, msg: &[u8]) -> Result<ExtendedSignature, signature::Error> {
+
        Ok(ExtendedSignature {
+
            key: self.public,
+
            sig: Signer::try_sign(self, msg).map_err(signature::Error::from_source)?,
+
        })
+
    }
+
}
+

impl AgentSigner {
    pub fn new(agent: Agent, public: PublicKey) -> Self {
        let agent = RefCell::new(agent);
modified radicle-crypto/src/ssh/keystore.rs
@@ -10,6 +10,8 @@ use zeroize::Zeroizing;

use crate::{KeyPair, PublicKey, SecretKey, Signature, Signer, SignerError};

+
use super::ExtendedSignature;
+

/// A secret key passphrase.
pub type Passphrase = Zeroizing<String>;

@@ -186,6 +188,21 @@ pub struct MemorySigner {
    secret: Zeroizing<SecretKey>,
}

+
impl signature::Signer<Signature> for MemorySigner {
+
    fn try_sign(&self, msg: &[u8]) -> Result<Signature, signature::Error> {
+
        Ok(Signer::sign(self, msg))
+
    }
+
}
+

+
impl signature::Signer<ExtendedSignature> for MemorySigner {
+
    fn try_sign(&self, msg: &[u8]) -> Result<ExtendedSignature, signature::Error> {
+
        Ok(ExtendedSignature {
+
            key: self.public,
+
            sig: Signer::sign(self, msg),
+
        })
+
    }
+
}
+

impl Signer for MemorySigner {
    fn public_key(&self) -> &PublicKey {
        &self.public
modified radicle-crypto/src/test/signer.rs
@@ -1,4 +1,6 @@
-
use crate::{KeyPair, PublicKey, SecretKey, Seed, Signature, Signer, SignerError};
+
use crate::{
+
    ssh::ExtendedSignature, KeyPair, PublicKey, SecretKey, Seed, Signature, Signer, SignerError,
+
};

#[derive(Debug, Clone)]
pub struct MockSigner {
@@ -6,6 +8,21 @@ pub struct MockSigner {
    sk: SecretKey,
}

+
impl signature::Signer<ExtendedSignature> for MockSigner {
+
    fn try_sign(&self, msg: &[u8]) -> Result<ExtendedSignature, signature::Error> {
+
        Ok(ExtendedSignature {
+
            key: self.pk,
+
            sig: signature::Signer::<Signature>::try_sign(self, msg)?,
+
        })
+
    }
+
}
+

+
impl signature::Signer<Signature> for MockSigner {
+
    fn try_sign(&self, msg: &[u8]) -> Result<Signature, signature::Error> {
+
        Ok(Signature(self.sk.sign(msg, None)))
+
    }
+
}
+

impl MockSigner {
    pub fn new(rng: &mut fastrand::Rng) -> Self {
        let mut seed: [u8; 32] = [0; 32];
modified radicle-node/src/lib.rs
@@ -34,7 +34,7 @@ pub const VERSION: Version = Version {

pub mod prelude {
    pub use crate::bounded::BoundedVec;
-
    pub use crate::crypto::{PublicKey, Signature, Signer};
+
    pub use crate::crypto::{PublicKey, Signature};
    pub use crate::deserializer::Deserializer;
    pub use crate::identity::{Did, RepoId};
    pub use crate::node::Address;
modified radicle-node/src/main.rs
@@ -5,7 +5,7 @@ use anyhow::Context;
use crossbeam_channel as chan;

use radicle::logger;
-
use radicle::prelude::Signer;
+
use radicle::node::device::Device;
use radicle::profile;
use radicle_node::crypto::ssh::keystore::{Keystore, MemorySigner};
use radicle_node::{Runtime, VERSION};
@@ -99,7 +99,9 @@ fn execute() -> anyhow::Result<()> {

    let passphrase = profile::env::passphrase();
    let keystore = Keystore::new(&home.keys());
-
    let signer = MemorySigner::load(&keystore, passphrase).context("couldn't load secret key")?;
+
    let signer = Device::from(
+
        MemorySigner::load(&keystore, passphrase).context("couldn't load secret key")?,
+
    );

    log::info!(target: "node", "Node ID is {}", signer.public_key());

modified radicle-node/src/runtime.rs
@@ -9,6 +9,8 @@ use crossbeam_channel as chan;
use cyphernet::Ecdh;
use netservices::resource::NetAccept;
use radicle::cob::migrate;
+
use radicle::crypto;
+
use radicle::node::device::Device;
use radicle_fetch::FetchLimit;
use radicle_signals::Signal;
use reactor::poller::popol;
@@ -25,7 +27,6 @@ use radicle::profile::Home;
use radicle::{cob, git, storage, Storage};

use crate::control;
-
use crate::crypto::Signer;
use crate::node::{routing, NodeId};
use crate::service::message::NodeAnnouncement;
use crate::service::{gossip, policy, Event, INITIAL_SUBSCRIBE_BACKLOG_DELTA};
@@ -119,10 +120,10 @@ impl Runtime {
        config: service::Config,
        listen: Vec<net::SocketAddr>,
        signals: chan::Receiver<Signal>,
-
        signer: G,
+
        signer: Device<G>,
    ) -> Result<Runtime, Error>
    where
-
        G: Signer + Ecdh<Pk = NodeId> + Clone + 'static,
+
        G: crypto::signature::Signer<crypto::Signature> + Ecdh<Pk = NodeId> + Clone + 'static,
    {
        let id = *signer.public_key();
        let alias = config.alias.clone();
modified radicle-node/src/service.rs
@@ -28,6 +28,7 @@ use radicle::node::address;
use radicle::node::address::Store as _;
use radicle::node::address::{AddressBook, AddressType, KnownAddress};
use radicle::node::config::PeerConfig;
+
use radicle::node::device::Device;
use radicle::node::refs::Store as _;
use radicle::node::routing::Store as _;
use radicle::node::seed;
@@ -37,7 +38,6 @@ use radicle::storage::refs::SIGREFS_BRANCH;
use radicle::storage::RepositoryError;
use radicle_fetch::policy::SeedingPolicy;

-
use crate::crypto::Signer;
use crate::identity::RepoId;
use crate::node::routing;
use crate::node::routing::InsertResult;
@@ -390,7 +390,7 @@ pub struct Service<D, S, G> {
    /// Service configuration.
    config: Config,
    /// Our cryptographic signer and key.
-
    signer: G,
+
    signer: Device<G>,
    /// Project storage.
    storage: S,
    /// Node database.
@@ -444,10 +444,7 @@ pub struct Service<D, S, G> {
    metrics: Metrics,
}

-
impl<D, S, G> Service<D, S, G>
-
where
-
    G: crypto::Signer,
-
{
+
impl<D, S, G> Service<D, S, G> {
    /// Get the local node id.
    pub fn node_id(&self) -> NodeId {
        *self.signer.public_key()
@@ -467,14 +464,14 @@ impl<D, S, G> Service<D, S, G>
where
    D: Store,
    S: ReadStorage + 'static,
-
    G: Signer,
+
    G: crypto::signature::Signer<crypto::Signature>,
{
    pub fn new(
        config: Config,
        db: Stores<D>,
        storage: S,
        policies: policy::Config<Write>,
-
        signer: G,
+
        signer: Device<G>,
        rng: Rng,
        node: NodeAnnouncement,
        emitter: Emitter<Event>,
@@ -596,7 +593,7 @@ where
    }

    /// Get the local signer.
-
    pub fn signer(&self) -> &G {
+
    pub fn signer(&self) -> &Device<G> {
        &self.signer
    }

@@ -2658,7 +2655,7 @@ pub trait ServiceState {
impl<D, S, G> ServiceState for Service<D, S, G>
where
    D: routing::Store,
-
    G: Signer,
+
    G: crypto::signature::Signer<crypto::Signature>,
    S: ReadStorage,
{
    fn nid(&self) -> &NodeId {
modified radicle-node/src/service/gossip/store.rs
@@ -399,7 +399,7 @@ mod test {
    use crate::test::arbitrary;
    use localtime::LocalTime;
    use radicle::assert_matches;
-
    use radicle_crypto::test::signer::MockSigner;
+
    use radicle::node::device::Device;

    #[test]
    fn test_announced() {
@@ -407,7 +407,7 @@ mod test {
        let nid = arbitrary::gen::<NodeId>(1);
        let rid = arbitrary::gen::<RepoId>(1);
        let timestamp = LocalTime::now().into();
-
        let signer = MockSigner::default();
+
        let signer = Device::mock();
        let refs = AnnouncementMessage::Refs(RefsAnnouncement {
            rid,
            refs: BoundedVec::new(),
modified radicle-node/src/service/message.rs
@@ -2,6 +2,7 @@ use std::{fmt, io, mem};

use nonempty::NonEmpty;
use radicle::git;
+
use radicle::node::device::Device;
use radicle::storage::refs::RefsAt;

use crate::crypto;
@@ -260,7 +261,12 @@ pub enum AnnouncementMessage {

impl AnnouncementMessage {
    /// Sign this announcement message.
-
    pub fn signed<G: crypto::Signer>(self, signer: &G) -> Announcement {
+
    pub fn signed<G>(self, signer: &Device<G>) -> Announcement
+
    where
+
        G: crypto::signature::Signer<crypto::Signature>,
+
    {
+
        use crypto::signature::Signer as _;
+

        let msg = wire::serialize(&self);
        let signature = signer.sign(&msg);

@@ -442,11 +448,17 @@ impl Message {
        .into()
    }

-
    pub fn node<G: crypto::Signer>(message: NodeAnnouncement, signer: &G) -> Self {
+
    pub fn node<G: crypto::signature::Signer<crypto::Signature>>(
+
        message: NodeAnnouncement,
+
        signer: &Device<G>,
+
    ) -> Self {
        AnnouncementMessage::from(message).signed(signer).into()
    }

-
    pub fn inventory<G: crypto::Signer>(message: InventoryAnnouncement, signer: &G) -> Self {
+
    pub fn inventory<G: crypto::signature::Signer<crypto::Signature>>(
+
        message: InventoryAnnouncement,
+
        signer: &Device<G>,
+
    ) -> Self {
        AnnouncementMessage::from(message).signed(signer).into()
    }

@@ -587,7 +599,6 @@ mod tests {
    use radicle::git::raw;

    use super::*;
-
    use crate::crypto::test::signer::MockSigner;
    use crate::prelude::*;
    use crate::test::arbitrary;
    use crate::wire::Encode;
@@ -595,7 +606,7 @@ mod tests {
    #[test]
    fn test_ref_remote_limit() {
        let mut refs = BoundedVec::<_, REF_REMOTE_LIMIT>::new();
-
        let signer = MockSigner::default();
+
        let signer = Device::mock();
        let at = raw::Oid::zero().into();

        assert_eq!(refs.capacity(), REF_REMOTE_LIMIT);
@@ -613,7 +624,7 @@ mod tests {
            refs,
            timestamp: LocalTime::now().into(),
        })
-
        .signed(&MockSigner::default())
+
        .signed(&Device::mock())
        .into();

        let mut buf: Vec<u8> = Vec::new();
@@ -633,7 +644,7 @@ mod tests {
                    .expect("size within bounds limit"),
                timestamp: LocalTime::now().into(),
            },
-
            &MockSigner::default(),
+
            &Device::mock(),
        );
        let mut buf: Vec<u8> = Vec::new();
        assert!(
@@ -655,7 +666,7 @@ mod tests {

    #[quickcheck]
    fn prop_refs_announcement_signing(rid: RepoId) {
-
        let signer = MockSigner::new(&mut fastrand::Rng::new());
+
        let signer = Device::mock_rng(&mut fastrand::Rng::new());
        let timestamp = Timestamp::EPOCH;
        let at = raw::Oid::zero().into();
        let refs = BoundedVec::collect_from(
modified radicle-node/src/test/environment.rs
@@ -11,12 +11,14 @@ use crossbeam_channel as chan;

use localtime::LocalTime;
use radicle::cob::{issue, migrate};
+
use radicle::crypto;
use radicle::crypto::ssh::{keystore::MemorySigner, Keystore};
use radicle::crypto::test::signer::MockSigner;
-
use radicle::crypto::{KeyPair, Seed, Signer};
+
use radicle::crypto::{KeyPair, Seed};
use radicle::git::refname;
use radicle::identity::{RepoId, Visibility};
use radicle::node::config::ConnectAddress;
+
use radicle::node::device::Device;
use radicle::node::policy::store as policy;
use radicle::node::seed::Store as _;
use radicle::node::{Alias, Database, UserAgent, POLICIES_DB_FILE};
@@ -160,7 +162,7 @@ impl Environment {
pub struct Node<G> {
    pub id: NodeId,
    pub home: Home,
-
    pub signer: G,
+
    pub signer: Device<G>,
    pub storage: Storage,
    pub config: Config,
    pub db: service::Stores<Database>,
@@ -169,7 +171,7 @@ pub struct Node<G> {

impl Node<MemorySigner> {
    pub fn new(profile: Profile) -> Self {
-
        let signer = MemorySigner::load(&profile.keystore, None).unwrap();
+
        let signer = Device::from(MemorySigner::load(&profile.keystore, None).unwrap());
        let id = *profile.id();
        let policies_db = profile.home.node().join(POLICIES_DB_FILE);
        let policies = policy::Store::open(policies_db).unwrap();
@@ -189,17 +191,19 @@ impl Node<MemorySigner> {
}

/// Handle to a running node.
-
pub struct NodeHandle<G: Signer + cyphernet::Ecdh + 'static> {
+
pub struct NodeHandle<G: crypto::signature::Signer<crypto::Signature> + cyphernet::Ecdh + 'static> {
    pub id: NodeId,
    pub storage: Storage,
-
    pub signer: G,
+
    pub signer: Device<G>,
    pub home: Home,
    pub addr: net::SocketAddr,
    pub thread: ManuallyDrop<thread::JoinHandle<Result<(), runtime::Error>>>,
    pub handle: ManuallyDrop<Handle>,
}

-
impl<G: Signer + cyphernet::Ecdh + 'static> Drop for NodeHandle<G> {
+
impl<G: crypto::signature::Signer<crypto::Signature> + cyphernet::Ecdh + 'static> Drop
+
    for NodeHandle<G>
+
{
    fn drop(&mut self) {
        log::debug!(target: "test", "Node {} shutting down..", self.id);

@@ -213,7 +217,7 @@ impl<G: Signer + cyphernet::Ecdh + 'static> Drop for NodeHandle<G> {
    }
}

-
impl<G: Signer + cyphernet::Ecdh> NodeHandle<G> {
+
impl<G: crypto::signature::Signer<crypto::Signature> + cyphernet::Ecdh> NodeHandle<G> {
    /// Connect this node to another node, and wait for the connection to be established both ways.
    pub fn connect(&mut self, remote: &NodeHandle<G>) -> &mut Self {
        let local_events = self.handle.events();
@@ -481,7 +485,7 @@ impl Node<MockSigner> {
                .collect::<String>(),
        );
        let home = Home::new(home).unwrap();
-
        let signer = MockSigner::default();
+
        let signer = Device::mock();
        let storage = Storage::open(
            home.storage(),
            git::UserInfo {
@@ -507,7 +511,10 @@ impl Node<MockSigner> {
    }
}

-
impl<G: cyphernet::Ecdh<Pk = NodeId> + Signer + Clone> Node<G> {
+
impl<G> Node<G>
+
where
+
    G: cyphernet::Ecdh<Pk = NodeId> + crypto::signature::Signer<crypto::Signature> + Clone,
+
{
    /// Spawn a node in its own thread.
    pub fn spawn(self) -> NodeHandle<G> {
        let listen = vec![([0, 0, 0, 0], 0).into()];
@@ -606,7 +613,7 @@ impl<G: cyphernet::Ecdh<Pk = NodeId> + Signer + Clone> Node<G> {

/// Checks whether the nodes have converged in their routing tables.
#[track_caller]
-
pub fn converge<'a, G: Signer + cyphernet::Ecdh + 'static>(
+
pub fn converge<'a, G: crypto::signature::Signer<crypto::Signature> + cyphernet::Ecdh + 'static>(
    nodes: impl IntoIterator<Item = &'a NodeHandle<G>>,
) -> BTreeSet<(RepoId, NodeId)> {
    let nodes = nodes.into_iter().collect::<Vec<_>>();
modified radicle-node/src/test/gossip.rs
@@ -1,7 +1,7 @@
use std::str::FromStr;

-
use radicle::crypto::test::signer::MockSigner;
use radicle::node;
+
use radicle::node::device::Device;
use radicle::node::UserAgent;
use radicle::test::fixtures::gen;

@@ -17,7 +17,7 @@ pub fn messages(count: usize, now: LocalTime, delta: LocalDuration) -> Vec<Messa
    let mut msgs = Vec::new();

    for _ in 0..count {
-
        let signer = MockSigner::new(&mut rng);
+
        let signer = Device::mock_rng(&mut rng);
        let time = if delta == LocalDuration::from_secs(0) {
            now
        } else {
modified radicle-node/src/test/peer.rs
@@ -7,8 +7,10 @@ use std::str::FromStr;

use log::*;

+
use radicle::crypto;
use radicle::identity::Visibility;
use radicle::node::address::Store as _;
+
use radicle::node::device::Device;
use radicle::node::Database;
use radicle::node::UserAgent;
use radicle::node::{address, Alias, ConnectOptions};
@@ -18,7 +20,6 @@ use radicle::storage::{ReadRepository, RemoteRepository};
use radicle::Storage;

use crate::crypto::test::signer::MockSigner;
-
use crate::crypto::Signer;
use crate::identity::RepoId;
use crate::node;
use crate::node::routing::Store as _;
@@ -56,7 +57,7 @@ pub struct Peer<S, G> {
impl<S, G> simulator::Peer<S, G> for Peer<S, G>
where
    S: WriteStorage + 'static,
-
    G: Signer + 'static,
+
    G: crypto::signature::Signer<crypto::Signature> + 'static,
{
    fn init(&mut self) {}

@@ -98,11 +99,11 @@ where
    }
}

-
pub struct Config<G: Signer + 'static> {
+
pub struct Config<G: crypto::signature::Signer<crypto::Signature> + 'static> {
    pub config: service::Config,
    pub local_time: LocalTime,
    pub policy: SeedingPolicy,
-
    pub signer: G,
+
    pub signer: Device<G>,
    pub rng: fastrand::Rng,
    pub tmp: tempfile::TempDir,
}
@@ -110,7 +111,7 @@ pub struct Config<G: Signer + 'static> {
impl Default for Config<MockSigner> {
    fn default() -> Self {
        let mut rng = fastrand::Rng::new();
-
        let signer = MockSigner::new(&mut rng);
+
        let signer = Device::mock_rng(&mut rng);
        let tmp = tempfile::TempDir::new().unwrap();
        let config = service::Config::test(Alias::from_str("mocky").unwrap());

@@ -125,7 +126,7 @@ impl Default for Config<MockSigner> {
    }
}

-
impl<G: Signer> Peer<Storage, G> {
+
impl<G: crypto::signature::Signer<crypto::Signature>> Peer<Storage, G> {
    pub fn project(&mut self, name: &str, description: &str) -> RepoId {
        radicle::storage::git::transport::local::register(self.storage().clone());

@@ -148,7 +149,7 @@ impl<G: Signer> Peer<Storage, G> {
impl<S, G> Peer<S, G>
where
    S: WriteStorage + 'static,
-
    G: Signer + 'static,
+
    G: crypto::signature::Signer<crypto::Signature> + 'static,
{
    pub fn config(
        name: &'static str,
@@ -387,7 +388,10 @@ where
        .expect("`inventory-announcement` must be sent");
    }

-
    pub fn connect_to<T: WriteStorage + 'static, H: Signer + 'static>(
+
    pub fn connect_to<
+
        T: WriteStorage + 'static,
+
        H: crypto::signature::Signer<crypto::Signature> + 'static,
+
    >(
        &mut self,
        peer: &Peer<T, H>,
    ) {
modified radicle-node/src/test/simulator.rs
@@ -14,7 +14,7 @@ use std::{fmt, io, net};
use localtime::{LocalDuration, LocalTime};
use log::*;

-
use crate::crypto::Signer;
+
use crate::crypto;
use crate::prelude::{Address, RepoId};
use crate::service::io::Io;
use crate::service::{DisconnectReason, Event, Message, Metrics, NodeId};
@@ -202,7 +202,11 @@ pub struct Simulation<S, G> {
    signer: PhantomData<G>,
}

-
impl<S: WriteStorage + 'static, G: Signer> Simulation<S, G> {
+
impl<S, G> Simulation<S, G>
+
where
+
    S: WriteStorage + 'static,
+
    G: crypto::signature::Signer<crypto::Signature>,
+
{
    /// Create a new simulation.
    pub fn new(time: LocalTime, rng: fastrand::Rng, opts: Options) -> Self {
        Self {
modified radicle-node/src/tests.rs
@@ -12,6 +12,7 @@ use netservices::Direction as Link;
use once_cell::sync::Lazy;
use radicle::identity::Visibility;
use radicle::node::address::Store as _;
+
use radicle::node::device::Device;
use radicle::node::refs::Store as _;
use radicle::node::routing::Store as _;
use radicle::node::{ConnectOptions, DEFAULT_TIMEOUT};
@@ -21,7 +22,6 @@ use radicle::test::arbitrary::gen;
use radicle::test::storage::MockRepository;

use crate::collections::{RandomMap, RandomSet};
-
use crate::crypto::test::signer::MockSigner;
use crate::identity::RepoId;
use crate::node;
use crate::node::config::*;
@@ -270,7 +270,7 @@ fn test_inventory_sync() {
        [7, 7, 7, 7],
        Storage::open(tmp.path().join("alice"), fixtures::user()).unwrap(),
    );
-
    let bob_signer = MockSigner::default();
+
    let bob_signer = Device::mock();
    let bob_storage = fixtures::storage(tmp.path().join("bob"), &bob_signer).unwrap();
    let bob = Peer::with_storage("bob", [8, 8, 8, 8], bob_storage);
    let now = LocalTime::now().into();
@@ -682,7 +682,7 @@ fn test_refs_announcement_relay_public() {

    let bob = {
        let mut rng = fastrand::Rng::new();
-
        let signer = MockSigner::new(&mut rng);
+
        let signer = Device::mock_rng(&mut rng);
        let storage = fixtures::storage(tmp.path().join("bob"), &signer).unwrap();

        Peer::config(
@@ -766,7 +766,7 @@ fn test_refs_announcement_relay_private() {

    let bob = {
        let mut rng = fastrand::Rng::new();
-
        let signer = MockSigner::new(&mut rng);
+
        let signer = Device::mock_rng(&mut rng);
        let storage = fixtures::storage(tmp.path().join("bob"), &signer).unwrap();

        Peer::config(
@@ -855,7 +855,7 @@ fn test_refs_announcement_fetch_trusted_no_inventory() {
    );
    let bob = {
        let mut rng = fastrand::Rng::new();
-
        let signer = MockSigner::new(&mut rng);
+
        let signer = Device::mock_rng(&mut rng);
        let storage = fixtures::storage(tmp.path().join("bob"), &signer).unwrap();

        Peer::config(
@@ -967,7 +967,7 @@ fn test_refs_announcement_no_subscribe() {
fn test_refs_announcement_offline() {
    let tmp = tempfile::tempdir().unwrap();
    let mut alice = {
-
        let signer = MockSigner::default();
+
        let signer = Device::mock();
        let storage = fixtures::storage(tmp.path().join("alice"), &signer).unwrap();

        Peer::config(
modified radicle-node/src/tests/e2e.rs
@@ -1,6 +1,6 @@
use std::{collections::HashSet, thread, time};

-
use radicle::crypto::{test::signer::MockSigner, Signer};
+
use radicle::node::device::Device;
use radicle::node::{Alias, ConnectResult, FetchResult, Handle as _, DEFAULT_TIMEOUT};
use radicle::storage::{
    ReadRepository, ReadStorage, RefUpdate, RemoteRepository, SignRepository, ValidateRepository,
@@ -281,7 +281,7 @@ fn test_replication_invalid() {
    let tmp = tempfile::tempdir().unwrap();
    let alice = Node::init(tmp.path(), config::relay("alice"));
    let mut bob = Node::init(tmp.path(), config::relay("bob"));
-
    let carol = MockSigner::default();
+
    let carol = Device::mock();
    let acme = bob.project("acme", "");
    let repo = bob.storage.repository_mut(acme).unwrap();
    let (_, head) = repo.head().unwrap();
@@ -418,7 +418,7 @@ fn test_fetch_followed_remotes() {
    let mut signers = Vec::with_capacity(5);
    {
        for _ in 0..5 {
-
            let signer = MockSigner::default();
+
            let signer = Device::mock();
            rad::fork_remote(acme, &alice.id, &signer, &alice.storage).unwrap();
            signers.push(signer);
        }
@@ -473,7 +473,7 @@ fn test_missing_remote() {

    let mut alice = alice.spawn();
    let mut bob = bob.spawn();
-
    let carol = MockSigner::default();
+
    let carol = Device::mock();

    alice.connect(&bob);
    converge([&alice, &bob]);
modified radicle-node/src/wire/message.rs
@@ -458,9 +458,9 @@ impl wire::Decode for ZeroBytes {
mod tests {
    use super::*;
    use qcheck_macros::quickcheck;
+
    use radicle::node::device::Device;
    use radicle::node::UserAgent;
    use radicle::storage::refs::RefsAt;
-
    use radicle_crypto::test::signer::MockSigner;

    use crate::deserializer::Deserializer;
    use crate::test::arbitrary;
@@ -468,7 +468,7 @@ mod tests {

    #[test]
    fn test_refs_ann_max_size() {
-
        let signer = MockSigner::default();
+
        let signer = Device::mock();
        let refs: [RefsAt; REF_REMOTE_LIMIT] = arbitrary::gen(1);
        let ann = AnnouncementMessage::Refs(RefsAnnouncement {
            rid: arbitrary::gen(1),
@@ -484,7 +484,7 @@ mod tests {

    #[test]
    fn test_inv_ann_max_size() {
-
        let signer = MockSigner::default();
+
        let signer = Device::mock();
        let inv: [RepoId; INVENTORY_LIMIT] = arbitrary::gen(1);
        let ann = AnnouncementMessage::Inventory(InventoryAnnouncement {
            inventory: BoundedVec::collect_from(&mut inv.into_iter()),
@@ -499,7 +499,7 @@ mod tests {

    #[test]
    fn test_node_ann_max_size() {
-
        let signer = MockSigner::default();
+
        let signer = Device::mock();
        let addrs: [Address; ADDRESS_LIMIT] = arbitrary::gen(1);
        let alias = ['@'; radicle::node::MAX_ALIAS_LENGTH];
        let ann = AnnouncementMessage::Node(NodeAnnouncement {
modified radicle-node/src/wire/protocol.rs
@@ -18,14 +18,15 @@ use localtime::LocalTime;
use netservices::resource::{ListenerEvent, NetAccept, NetTransport, SessionEvent};
use netservices::session::{NoiseSession, ProtocolArtifact, Socks5Session};
use netservices::{NetConnection, NetReader, NetWriter};
+
use radicle::node::device::Device;
use reactor::{ResourceId, ResourceType, Timestamp};

use radicle::collections::RandomMap;
+
use radicle::crypto;
use radicle::node::config::AddressConfig;
use radicle::node::NodeId;
use radicle::storage::WriteStorage;

-
use crate::crypto::Signer;
use crate::prelude::Deserializer;
use crate::service;
use crate::service::io::Io;
@@ -306,13 +307,13 @@ impl Peers {
}

/// Wire protocol implementation for a set of peers.
-
pub struct Wire<D, S, G: Signer + Ecdh> {
+
pub struct Wire<D, S, G: crypto::signature::Signer<crypto::Signature> + Ecdh> {
    /// Backing service instance.
    service: Service<D, S, G>,
    /// Worker pool interface.
    worker: chan::Sender<Task>,
    /// Used for authentication.
-
    signer: G,
+
    signer: Device<G>,
    /// Node metrics.
    metrics: service::Metrics,
    /// Internal queue of actions to send to the reactor.
@@ -331,9 +332,9 @@ impl<D, S, G> Wire<D, S, G>
where
    D: service::Store,
    S: WriteStorage + 'static,
-
    G: Signer + Ecdh<Pk = NodeId>,
+
    G: crypto::signature::Signer<crypto::Signature> + Ecdh<Pk = NodeId>,
{
-
    pub fn new(service: Service<D, S, G>, worker: chan::Sender<Task>, signer: G) -> Self {
+
    pub fn new(service: Service<D, S, G>, worker: chan::Sender<Task>, signer: Device<G>) -> Self {
        assert!(service.started().is_some(), "Service must be initialized");

        Self {
@@ -500,7 +501,7 @@ impl<D, S, G> reactor::Handler for Wire<D, S, G>
where
    D: service::Store + Send,
    S: WriteStorage + Send + 'static,
-
    G: Signer + Ecdh<Pk = NodeId> + Clone + Send,
+
    G: crypto::signature::Signer<crypto::Signature> + Ecdh<Pk = NodeId> + Clone + Send,
{
    type Listener = NetAccept<WireSession<G>>;
    type Transport = NetTransport<WireSession<G>>;
@@ -561,14 +562,17 @@ where
                    return;
                }

-
                let session =
-
                    match accept::<G>(remote.clone().into(), connection, self.signer.clone()) {
-
                        Ok(s) => s,
-
                        Err(e) => {
-
                            log::error!(target: "wire", "Error creating session for {ip}: {e}");
-
                            return;
-
                        }
-
                    };
+
                let session = match accept::<G>(
+
                    remote.clone().into(),
+
                    connection,
+
                    self.signer.clone().into_inner(),
+
                ) {
+
                    Ok(s) => s,
+
                    Err(e) => {
+
                        log::error!(target: "wire", "Error creating session for {ip}: {e}");
+
                        return;
+
                    }
+
                };
                let transport = match NetTransport::with_session(session, Link::Inbound) {
                    Ok(transport) => transport,
                    Err(err) => {
@@ -963,7 +967,7 @@ impl<D, S, G> Iterator for Wire<D, S, G>
where
    D: service::Store,
    S: WriteStorage + 'static,
-
    G: Signer + Ecdh<Pk = NodeId>,
+
    G: crypto::signature::Signer<crypto::Signature> + Ecdh<Pk = NodeId> + Clone,
{
    type Item = Action<G>;

@@ -1016,7 +1020,7 @@ where
                    match dial::<G>(
                        addr.to_inner(),
                        node_id,
-
                        self.signer.clone(),
+
                        self.signer.clone().into_inner(),
                        self.service.config(),
                    )
                    .and_then(|session| {
@@ -1126,7 +1130,7 @@ where
}

/// Establish a new outgoing connection.
-
pub fn dial<G: Signer + Ecdh<Pk = NodeId>>(
+
pub fn dial<G: Ecdh<Pk = NodeId>>(
    remote_addr: NetAddr<HostName>,
    remote_id: <G as EcSk>::Pk,
    signer: G,
@@ -1185,7 +1189,7 @@ pub fn dial<G: Signer + Ecdh<Pk = NodeId>>(
}

/// Accept a new connection.
-
pub fn accept<G: Signer + Ecdh<Pk = NodeId>>(
+
pub fn accept<G: Ecdh<Pk = NodeId>>(
    remote_addr: NetAddr<HostName>,
    connection: net::TcpStream,
    signer: G,
@@ -1194,7 +1198,7 @@ pub fn accept<G: Signer + Ecdh<Pk = NodeId>>(
}

/// Create a new [`WireSession`].
-
fn session<G: Signer + Ecdh<Pk = NodeId>>(
+
fn session<G: Ecdh<Pk = NodeId>>(
    remote_addr: NetAddr<HostName>,
    remote_id: Option<NodeId>,
    connection: net::TcpStream,
modified radicle-node/src/worker/fetch.rs
@@ -8,6 +8,7 @@ use localtime::LocalTime;
use radicle::cob::TypedId;
use radicle::crypto::PublicKey;
use radicle::identity::DocAt;
+
use radicle::prelude::NodeId;
use radicle::prelude::RepoId;
use radicle::storage::refs::RefsAt;
use radicle::storage::{
@@ -299,7 +300,7 @@ fn cache_cobs<S, C>(
    cache: &mut C,
) -> Result<(), error::Cache>
where
-
    S: ReadRepository + cob::Store,
+
    S: ReadRepository + cob::Store<Namespace = NodeId>,
    C: cob::cache::Update<cob::issue::Issue> + cob::cache::Update<cob::patch::Patch>,
    C: cob::cache::Remove<cob::issue::Issue> + cob::cache::Remove<cob::patch::Patch>,
{
modified radicle-remote-helper/src/lib.rs
@@ -13,6 +13,7 @@ use std::{env, fmt, io};

use thiserror::Error;

+
use radicle::prelude::NodeId;
use radicle::storage::git::transport::local::{Url, UrlError};
use radicle::storage::{ReadRepository, WriteStorage};
use radicle::{cob, profile};
@@ -262,7 +263,7 @@ pub(crate) fn warn(s: impl fmt::Display) {
}

/// Get the patch store.
-
pub(crate) fn patches<'a, R: ReadRepository + cob::Store>(
+
pub(crate) fn patches<'a, R: ReadRepository + cob::Store<Namespace = NodeId>>(
    profile: &Profile,
    repo: &'a R,
) -> Result<cob::patch::Cache<cob::patch::Patches<'a, R>, cob::cache::StoreReader>, list::Error> {
modified radicle-remote-helper/src/list.rs
@@ -4,6 +4,7 @@ use thiserror::Error;

use radicle::cob;
use radicle::git;
+
use radicle::prelude::NodeId;
use radicle::storage::git::transport::local::Url;
use radicle::storage::ReadRepository;
use radicle::Profile;
@@ -34,7 +35,7 @@ pub enum Error {
}

/// List refs for fetching (`git fetch` and `git ls-remote`).
-
pub fn for_fetch<R: ReadRepository + cob::Store + 'static>(
+
pub fn for_fetch<R: ReadRepository + cob::Store<Namespace = NodeId> + 'static>(
    url: &Url,
    profile: &Profile,
    stored: &R,
@@ -83,7 +84,7 @@ pub fn for_push<R: ReadRepository>(profile: &Profile, stored: &R) -> Result<(),
}

/// List canonical patch references. These are magic refs that can be used to pull patch updates.
-
fn patch_refs<R: ReadRepository + cob::Store + 'static>(
+
fn patch_refs<R: ReadRepository + cob::Store<Namespace = NodeId> + 'static>(
    profile: &Profile,
    stored: &R,
) -> Result<(), Error> {
modified radicle-remote-helper/src/push.rs
@@ -5,13 +5,14 @@ use std::path::{Path, PathBuf};
use std::str::FromStr;
use std::{assert_eq, io};

+
use radicle::node::device::Device;
use thiserror::Error;

use radicle::cob;
use radicle::cob::object::ParseObjectId;
use radicle::cob::patch;
use radicle::cob::patch::cache::Patches as _;
-
use radicle::crypto::Signer;
+
use radicle::crypto;
use radicle::explorer::ExplorerResource;
use radicle::git::canonical;
use radicle::git::canonical::Canonical;
@@ -389,7 +390,7 @@ pub fn run(
}

/// Open a new patch.
-
fn patch_open<G: Signer>(
+
fn patch_open<G>(
    src: &git::RefStr,
    upstream: &git::RefString,
    nid: &NodeId,
@@ -399,10 +400,13 @@ fn patch_open<G: Signer>(
        patch::Patches<'_, storage::git::Repository>,
        cob::cache::StoreWriter,
    >,
-
    signer: &G,
+
    signer: &Device<G>,
    profile: &Profile,
    opts: Options,
-
) -> Result<Option<ExplorerResource>, Error> {
+
) -> Result<Option<ExplorerResource>, Error>
+
where
+
    G: crypto::signature::Signer<crypto::Signature>,
+
{
    let reference = working.find_reference(src.as_str())?;
    let commit = reference.peel_to_commit()?;
    let dst = git::refs::storage::staging::patch(nid, commit.id());
@@ -509,7 +513,7 @@ fn patch_open<G: Signer>(

/// Update an existing patch.
#[allow(clippy::too_many_arguments)]
-
fn patch_update<G: Signer>(
+
fn patch_update<G>(
    src: &git::RefStr,
    dst: &git::Qualified,
    force: bool,
@@ -521,9 +525,12 @@ fn patch_update<G: Signer>(
        patch::Patches<'_, storage::git::Repository>,
        cob::cache::StoreWriter,
    >,
-
    signer: &G,
+
    signer: &Device<G>,
    opts: Options,
-
) -> Result<Option<ExplorerResource>, Error> {
+
) -> Result<Option<ExplorerResource>, Error>
+
where
+
    G: crypto::signature::Signer<crypto::Signature>,
+
{
    let reference = working.find_reference(src.as_str())?;
    let commit = reference.peel_to_commit()?;
    let patch_id = radicle::cob::ObjectId::from(oid);
@@ -592,7 +599,7 @@ fn patch_update<G: Signer>(
    Ok(Some(ExplorerResource::Patch { id: patch_id }))
}

-
fn push<G: Signer>(
+
fn push<G>(
    src: &git::RefStr,
    dst: &git::Qualified,
    force: bool,
@@ -603,8 +610,11 @@ fn push<G: Signer>(
        patch::Patches<'_, storage::git::Repository>,
        cob::cache::StoreWriter,
    >,
-
    signer: &G,
-
) -> Result<Option<ExplorerResource>, Error> {
+
    signer: &Device<G>,
+
) -> Result<Option<ExplorerResource>, Error>
+
where
+
    G: crypto::signature::Signer<crypto::Signature>,
+
{
    let head = match working.find_reference(src.as_str()) {
        Ok(obj) => obj.peel_to_commit()?,
        Err(e) => {
@@ -648,7 +658,7 @@ fn push<G: Signer>(
}

/// Revert all patches that are no longer included in the base branch.
-
fn patch_revert_all<G: Signer>(
+
fn patch_revert_all<G>(
    old: git::Oid,
    new: git::Oid,
    stored: &git::raw::Repository,
@@ -656,8 +666,11 @@ fn patch_revert_all<G: Signer>(
        patch::Patches<'_, storage::git::Repository>,
        cob::cache::StoreWriter,
    >,
-
    _signer: &G,
-
) -> Result<(), Error> {
+
    _signer: &Device<G>,
+
) -> Result<(), Error>
+
where
+
    G: crypto::signature::Signer<crypto::Signature>,
+
{
    // Find all commits reachable from the old OID but not from the new OID.
    let mut revwalk = stored.revwalk()?;
    revwalk.push(*old)?;
@@ -710,7 +723,7 @@ fn patch_revert_all<G: Signer>(
}

/// Merge all patches that have been included in the base branch.
-
fn patch_merge_all<G: Signer>(
+
fn patch_merge_all<G>(
    old: git::Oid,
    new: git::Oid,
    working: &git::raw::Repository,
@@ -718,8 +731,11 @@ fn patch_merge_all<G: Signer>(
        patch::Patches<'_, storage::git::Repository>,
        cob::cache::StoreWriter,
    >,
-
    signer: &G,
-
) -> Result<(), Error> {
+
    signer: &Device<G>,
+
) -> Result<(), Error>
+
where
+
    G: crypto::signature::Signer<crypto::Signature>,
+
{
    let mut revwalk = working.revwalk()?;
    revwalk.push_range(&format!("{old}..{new}"))?;

@@ -760,13 +776,17 @@ fn patch_merge_all<G: Signer>(
    Ok(())
}

-
fn patch_merge<C: cob::cache::Update<patch::Patch>, G: Signer>(
+
fn patch_merge<C, G>(
    mut patch: patch::PatchMut<storage::git::Repository, C>,
    revision: patch::RevisionId,
    commit: git::Oid,
    working: &git::raw::Repository,
-
    signer: &G,
-
) -> Result<(), Error> {
+
    signer: &Device<G>,
+
) -> Result<(), Error>
+
where
+
    C: cob::cache::Update<patch::Patch>,
+
    G: crypto::signature::Signer<crypto::Signature>,
+
{
    let (latest, _) = patch.latest();
    let merged = patch.merge(revision, commit, signer)?;

modified radicle/src/cob/identity.rs
@@ -4,13 +4,14 @@ use std::{fmt, ops::Deref, str::FromStr};
use crypto::{PublicKey, Signature};
use once_cell::sync::Lazy;
use radicle_cob::{Embed, ObjectId, TypeName};
-
use radicle_crypto::Signer;
use radicle_git_ext as git_ext;
use radicle_git_ext::Oid;
use serde::{Deserialize, Serialize};
use thiserror::Error;

use crate::identity::doc::Doc;
+
use crate::node::device::Device;
+
use crate::node::NodeId;
use crate::{
    cob,
    cob::{
@@ -192,11 +193,15 @@ impl Identity {
        }
    }

-
    pub fn initialize<'a, R: WriteRepository + cob::Store, G: Signer>(
+
    pub fn initialize<'a, R, G>(
        doc: &Doc,
        store: &'a R,
-
        signer: &G,
-
    ) -> Result<IdentityMut<'a, R>, cob::store::Error> {
+
        signer: &Device<G>,
+
    ) -> Result<IdentityMut<'a, R>, cob::store::Error>
+
    where
+
        G: crypto::signature::Signer<crypto::Signature>,
+
        R: WriteRepository + cob::Store<Namespace = NodeId>,
+
    {
        let mut store = cob::store::Store::open(store)?;
        let (id, identity) = Transaction::<Identity, _>::initial(
            "Initialize identity",
@@ -224,7 +229,7 @@ impl Identity {
    }

    /// Get a proposal mutably.
-
    pub fn get_mut<'a, R: WriteRepository + cob::Store>(
+
    pub fn get_mut<'a, R: WriteRepository + cob::Store<Namespace = NodeId>>(
        id: &ObjectId,
        repo: &'a R,
    ) -> Result<IdentityMut<'a, R>, store::Error> {
@@ -245,7 +250,7 @@ impl Identity {
        Self::get(&oid, repo).map_err(RepositoryError::from)
    }

-
    pub fn load_mut<R: WriteRepository + cob::Store>(
+
    pub fn load_mut<R: WriteRepository + cob::Store<Namespace = NodeId>>(
        repo: &R,
    ) -> Result<IdentityMut<R>, RepositoryError> {
        let oid = repo.identity_root()?;
@@ -732,7 +737,10 @@ impl Revision {
        })
    }

-
    pub fn sign<G: Signer>(&self, signer: &G) -> Result<Signature, DocError> {
+
    pub fn sign<G: crypto::signature::Signer<crypto::Signature>>(
+
        &self,
+
        signer: &G,
+
    ) -> Result<Signature, DocError> {
        self.doc.signature_of(signer)
    }
}
@@ -839,14 +847,14 @@ impl<R: ReadRepository> store::Transaction<Identity, R> {
}

impl<R: WriteRepository> store::Transaction<Identity, R> {
-
    pub fn revision<G: Signer>(
+
    pub fn revision<G: crypto::signature::Signer<crypto::Signature>>(
        &mut self,
        title: impl ToString,
        description: impl ToString,
        doc: &Doc,
        parent: Option<RevisionId>,
        repo: &R,
-
        signer: &G,
+
        signer: &Device<G>,
    ) -> Result<(), store::Error> {
        let (blob, bytes, signature) = doc.sign(signer).map_err(store::Error::Identity)?;
        // Store document blob in repository.
@@ -886,7 +894,7 @@ impl<R> fmt::Debug for IdentityMut<'_, R> {

impl<R> IdentityMut<'_, R>
where
-
    R: WriteRepository + cob::Store,
+
    R: WriteRepository + cob::Store<Namespace = NodeId>,
{
    /// Reload the identity data from storage.
    pub fn reload(&mut self) -> Result<(), store::Error> {
@@ -901,11 +909,11 @@ where
    pub fn transaction<G, F>(
        &mut self,
        message: &str,
-
        signer: &G,
+
        signer: &Device<G>,
        operations: F,
    ) -> Result<EntryId, Error>
    where
-
        G: Signer,
+
        G: crypto::signature::Signer<crypto::Signature>,
        F: FnOnce(&mut Transaction<Identity, R>, &R) -> Result<(), store::Error>,
    {
        let mut tx = Transaction::default();
@@ -919,13 +927,16 @@ where

    /// Update the identity by proposing a new revision.
    /// If the signer is the only delegate, the revision is accepted automatically.
-
    pub fn update<G: Signer>(
+
    pub fn update<G>(
        &mut self,
        title: impl ToString,
        description: impl ToString,
        doc: &Doc,
-
        signer: &G,
-
    ) -> Result<RevisionId, Error> {
+
        signer: &Device<G>,
+
    ) -> Result<RevisionId, Error>
+
    where
+
        G: crypto::signature::Signer<crypto::Signature>,
+
    {
        let parent = self.current;
        let id = self.transaction("Propose revision", signer, |tx, repo| {
            tx.revision(title, description, doc, Some(parent), repo, signer)
@@ -935,11 +946,10 @@ where
    }

    /// Accept an active revision.
-
    pub fn accept<G: Signer>(
-
        &mut self,
-
        revision: &RevisionId,
-
        signer: &G,
-
    ) -> Result<EntryId, Error> {
+
    pub fn accept<G>(&mut self, revision: &RevisionId, signer: &Device<G>) -> Result<EntryId, Error>
+
    where
+
        G: crypto::signature::Signer<crypto::Signature>,
+
    {
        let id = *revision;
        let revision = self.revision(revision).ok_or(Error::NotFound(id))?;
        let signature = revision.sign(signer)?;
@@ -948,31 +958,32 @@ where
    }

    /// Reject an active revision.
-
    pub fn reject<G: Signer>(
-
        &mut self,
-
        revision: RevisionId,
-
        signer: &G,
-
    ) -> Result<EntryId, Error> {
+
    pub fn reject<G>(&mut self, revision: RevisionId, signer: &Device<G>) -> Result<EntryId, Error>
+
    where
+
        G: crypto::signature::Signer<crypto::Signature>,
+
    {
        self.transaction("Reject revision", signer, |tx, _| tx.reject(revision))
    }

    /// Redact a revision.
-
    pub fn redact<G: Signer>(
-
        &mut self,
-
        revision: RevisionId,
-
        signer: &G,
-
    ) -> Result<EntryId, Error> {
+
    pub fn redact<G>(&mut self, revision: RevisionId, signer: &Device<G>) -> Result<EntryId, Error>
+
    where
+
        G: crypto::signature::Signer<crypto::Signature>,
+
    {
        self.transaction("Redact revision", signer, |tx, _| tx.redact(revision))
    }

    /// Edit an active revision's title or description.
-
    pub fn edit<G: Signer>(
+
    pub fn edit<G>(
        &mut self,
        revision: RevisionId,
        title: String,
        description: String,
-
        signer: &G,
-
    ) -> Result<EntryId, Error> {
+
        signer: &Device<G>,
+
    ) -> Result<EntryId, Error>
+
    where
+
        G: crypto::signature::Signer<crypto::Signature>,
+
    {
        self.transaction("Edit revision", signer, |tx, _| {
            tx.edit(revision, title, description)
        })
@@ -1021,8 +1032,6 @@ mod lookup {
#[allow(clippy::unwrap_used)]
mod test {
    use qcheck_macros::quickcheck;
-
    use radicle_crypto::test::signer::MockSigner;
-
    use radicle_crypto::Signer as _;

    use crate::cob;
    use crate::crypto::PublicKey;
@@ -1052,7 +1061,7 @@ mod test {
    #[test]
    fn test_identity_updates() {
        let NodeWithRepo { node, repo } = NodeWithRepo::default();
-
        let bob = MockSigner::default();
+
        let bob = Device::mock();
        let signer = &node.signer;
        let mut identity = Identity::load_mut(&*repo).unwrap();
        let mut doc = identity.doc().clone().edit();
@@ -1112,8 +1121,8 @@ mod test {
    #[test]
    fn test_identity_update_rejected() {
        let NodeWithRepo { node, repo } = NodeWithRepo::default();
-
        let bob = MockSigner::default();
-
        let eve = MockSigner::default();
+
        let bob = Device::mock();
+
        let eve = Device::mock();
        let signer = &node.signer;
        let mut identity = Identity::load_mut(&*repo).unwrap();
        let mut doc = identity.doc().clone().edit();
@@ -1535,9 +1544,9 @@ mod test {
        let tempdir = tempfile::tempdir().unwrap();
        let mut rng = fastrand::Rng::new();

-
        let alice = MockSigner::new(&mut rng);
-
        let bob = MockSigner::new(&mut rng);
-
        let eve = MockSigner::new(&mut rng);
+
        let alice = Device::mock_rng(&mut rng);
+
        let bob = Device::mock_rng(&mut rng);
+
        let eve = Device::mock_rng(&mut rng);

        let storage = Storage::open(tempdir.path().join("storage"), fixtures::user()).unwrap();
        let (id, _, _, _) =
modified radicle/src/cob/issue.rs
@@ -15,8 +15,9 @@ use crate::cob::store::{Cob, CobAction};
use crate::cob::thread;
use crate::cob::thread::{Comment, CommentId, Thread};
use crate::cob::{op, store, ActorId, Embed, EntryId, ObjectId, TypeName};
-
use crate::crypto::Signer;
use crate::identity::doc::DocError;
+
use crate::node::device::Device;
+
use crate::node::NodeId;
use crate::prelude::{Did, Doc, ReadRepository, RepoId};
use crate::storage::{HasRepoId, RepositoryError, WriteRepository};

@@ -589,7 +590,7 @@ impl<R, C> std::fmt::Debug for IssueMut<'_, '_, R, C> {

impl<R, C> IssueMut<'_, '_, R, C>
where
-
    R: WriteRepository + cob::Store,
+
    R: WriteRepository + cob::Store<Namespace = NodeId>,
    C: cob::cache::Update<Issue>,
{
    /// Reload the issue data from storage.
@@ -608,26 +609,35 @@ where
    }

    /// Assign one or more actors to an issue.
-
    pub fn assign<G: Signer>(
+
    pub fn assign<G>(
        &mut self,
        assignees: impl IntoIterator<Item = Did>,
-
        signer: &G,
-
    ) -> Result<EntryId, Error> {
+
        signer: &Device<G>,
+
    ) -> Result<EntryId, Error>
+
    where
+
        G: crypto::signature::Signer<crypto::Signature>,
+
    {
        self.transaction("Assign", signer, |tx| tx.assign(assignees))
    }

    /// Set the issue title.
-
    pub fn edit<G: Signer>(&mut self, title: impl ToString, signer: &G) -> Result<EntryId, Error> {
+
    pub fn edit<G>(&mut self, title: impl ToString, signer: &Device<G>) -> Result<EntryId, Error>
+
    where
+
        G: crypto::signature::Signer<crypto::Signature>,
+
    {
        self.transaction("Edit", signer, |tx| tx.edit(title))
    }

    /// Set the issue description.
-
    pub fn edit_description<G: Signer>(
+
    pub fn edit_description<G>(
        &mut self,
        description: impl ToString,
        embeds: impl IntoIterator<Item = Embed<Uri>>,
-
        signer: &G,
-
    ) -> Result<EntryId, Error> {
+
        signer: &Device<G>,
+
    ) -> Result<EntryId, Error>
+
    where
+
        G: crypto::signature::Signer<crypto::Signature>,
+
    {
        let (id, _) = self.root();
        let id = *id;
        self.transaction("Edit description", signer, |tx| {
@@ -636,73 +646,89 @@ where
    }

    /// Lifecycle an issue.
-
    pub fn lifecycle<G: Signer>(&mut self, state: State, signer: &G) -> Result<EntryId, Error> {
+
    pub fn lifecycle<G>(&mut self, state: State, signer: &Device<G>) -> Result<EntryId, Error>
+
    where
+
        G: crypto::signature::Signer<crypto::Signature>,
+
    {
        self.transaction("Lifecycle", signer, |tx| tx.lifecycle(state))
    }

    /// Comment on an issue.
-
    pub fn comment<G: Signer, S: ToString>(
+
    pub fn comment<G, S>(
        &mut self,
        body: S,
        reply_to: CommentId,
        embeds: impl IntoIterator<Item = Embed<Uri>>,
-
        signer: &G,
-
    ) -> Result<EntryId, Error> {
+
        signer: &Device<G>,
+
    ) -> Result<EntryId, Error>
+
    where
+
        G: crypto::signature::Signer<crypto::Signature>,
+
        S: ToString,
+
    {
        self.transaction("Comment", signer, |tx| {
            tx.comment(body, reply_to, embeds.into_iter().collect())
        })
    }

    /// Edit a comment.
-
    pub fn edit_comment<G: Signer, S: ToString>(
+
    pub fn edit_comment<G, S>(
        &mut self,
        id: CommentId,
        body: S,
        embeds: impl IntoIterator<Item = Embed<Uri>>,
-
        signer: &G,
-
    ) -> Result<EntryId, Error> {
+
        signer: &Device<G>,
+
    ) -> Result<EntryId, Error>
+
    where
+
        G: crypto::signature::Signer<crypto::Signature>,
+
        S: ToString,
+
    {
        self.transaction("Edit comment", signer, |tx| {
            tx.edit_comment(id, body, embeds.into_iter().collect())
        })
    }

    /// Redact a comment.
-
    pub fn redact_comment<G: Signer>(
-
        &mut self,
-
        id: CommentId,
-
        signer: &G,
-
    ) -> Result<EntryId, Error> {
+
    pub fn redact_comment<G>(&mut self, id: CommentId, signer: &Device<G>) -> Result<EntryId, Error>
+
    where
+
        G: crypto::signature::Signer<crypto::Signature>,
+
    {
        self.transaction("Redact comment", signer, |tx| tx.redact_comment(id))
    }

    /// Label an issue.
-
    pub fn label<G: Signer>(
+
    pub fn label<G>(
        &mut self,
        labels: impl IntoIterator<Item = Label>,
-
        signer: &G,
-
    ) -> Result<EntryId, Error> {
+
        signer: &Device<G>,
+
    ) -> Result<EntryId, Error>
+
    where
+
        G: crypto::signature::Signer<crypto::Signature>,
+
    {
        self.transaction("Label", signer, |tx| tx.label(labels))
    }

    /// React to an issue comment.
-
    pub fn react<G: Signer>(
+
    pub fn react<G>(
        &mut self,
        to: CommentId,
        reaction: Reaction,
        active: bool,
-
        signer: &G,
-
    ) -> Result<EntryId, Error> {
+
        signer: &Device<G>,
+
    ) -> Result<EntryId, Error>
+
    where
+
        G: crypto::signature::Signer<crypto::Signature>,
+
    {
        self.transaction("React", signer, |tx| tx.react(to, reaction, active))
    }

    pub fn transaction<G, F>(
        &mut self,
        message: &str,
-
        signer: &G,
+
        signer: &Device<G>,
        operations: F,
    ) -> Result<EntryId, Error>
    where
-
        G: Signer,
+
        G: crypto::signature::Signer<crypto::Signature>,
        F: FnOnce(&mut Transaction<Issue, R>) -> Result<(), store::Error>,
    {
        let mut tx = Transaction::default();
@@ -767,7 +793,7 @@ impl IssueCounts {

impl<'a, R> Issues<'a, R>
where
-
    R: ReadRepository + cob::Store,
+
    R: ReadRepository + cob::Store<Namespace = NodeId>,
{
    /// Open an issues store.
    pub fn open(repository: &'a R) -> Result<Self, RepositoryError> {
@@ -780,7 +806,7 @@ where

impl<'a, R> Issues<'a, R>
where
-
    R: WriteRepository + cob::Store,
+
    R: WriteRepository + cob::Store<Namespace = NodeId>,
{
    /// Create a new issue.
    pub fn create<'g, G, C>(
@@ -791,10 +817,10 @@ where
        assignees: &[Did],
        embeds: impl IntoIterator<Item = Embed<Uri>>,
        cache: &'g mut C,
-
        signer: &G,
+
        signer: &Device<G>,
    ) -> Result<IssueMut<'a, 'g, R, C>, Error>
    where
-
        G: Signer,
+
        G: crypto::signature::Signer<crypto::Signature>,
        C: cob::cache::Update<Issue>,
    {
        let (id, issue) = Transaction::initial("Create issue", &mut self.raw, signer, |tx, _| {
@@ -822,9 +848,10 @@ where
    }

    /// Remove an issue.
-
    pub fn remove<C, G: Signer>(&self, id: &ObjectId, signer: &G) -> Result<(), store::Error>
+
    pub fn remove<C, G>(&self, id: &ObjectId, signer: &Device<G>) -> Result<(), store::Error>
    where
        C: cob::cache::Remove<Issue>,
+
        G: crypto::signature::Signer<crypto::Signature>,
    {
        self.raw.remove(id, signer)
    }
@@ -1717,13 +1744,12 @@ mod test {

    #[test]
    fn test_invalid_cob() {
-
        use crate::crypto::test::signer::MockSigner;
        use cob::change::Storage as _;
        use cob::object::Storage as _;
        use nonempty::NonEmpty;

        let test::setup::NodeWithRepo { node, repo, .. } = test::setup::NodeWithRepo::default();
-
        let eve = MockSigner::default();
+
        let eve = Device::mock();
        let identity = repo.identity().unwrap().head();
        let missing = arbitrary::oid();
        let type_name = Issue::type_name().clone();
modified radicle/src/cob/issue/cache.rs
@@ -9,7 +9,8 @@ use crate::cob::cache;
use crate::cob::cache::{Remove, StoreReader, StoreWriter, Update};
use crate::cob::store;
use crate::cob::{Embed, Label, ObjectId, TypeName, Uri};
-
use crate::crypto::Signer;
+
use crate::node::device::Device;
+
use crate::node::NodeId;
use crate::prelude::{Did, RepoId};
use crate::storage::{HasRepoId, ReadRepository, RepositoryError, SignRepository, WriteRepository};

@@ -104,11 +105,11 @@ impl<'a, R, C> Cache<super::Issues<'a, R>, C> {
        labels: &[Label],
        assignees: &[Did],
        embeds: impl IntoIterator<Item = Embed<Uri>>,
-
        signer: &G,
+
        signer: &Device<G>,
    ) -> Result<IssueMut<'a, 'g, R, C>, super::Error>
    where
-
        R: ReadRepository + WriteRepository + cob::Store,
-
        G: Signer,
+
        R: ReadRepository + WriteRepository + cob::Store<Namespace = NodeId>,
+
        G: crypto::signature::Signer<crypto::Signature>,
        C: Update<Issue>,
    {
        self.store.create(
@@ -124,10 +125,10 @@ impl<'a, R, C> Cache<super::Issues<'a, R>, C> {

    /// Remove the given `id` from the [`super::Issues`] storage, and
    /// removing the entry from the `cache`.
-
    pub fn remove<G>(&mut self, id: &IssueId, signer: &G) -> Result<(), super::Error>
+
    pub fn remove<G>(&mut self, id: &IssueId, signer: &Device<G>) -> Result<(), super::Error>
    where
-
        G: Signer,
-
        R: ReadRepository + SignRepository + cob::Store,
+
        G: crypto::signature::Signer<crypto::Signature>,
+
        R: ReadRepository + SignRepository + cob::Store<Namespace = NodeId>,
        C: Remove<Issue>,
    {
        self.store.remove(id, signer)?;
@@ -199,7 +200,7 @@ impl<'a, R, C> Cache<super::Issues<'a, R>, C> {

impl<'a, R> Cache<super::Issues<'a, R>, cache::NoCache>
where
-
    R: ReadRepository + cob::Store,
+
    R: ReadRepository + cob::Store<Namespace = NodeId>,
{
    /// Get a `Cache` that does no write-through modifications and
    /// uses the [`super::Issues`] store for all reads and writes.
modified radicle/src/cob/job.rs
@@ -19,8 +19,9 @@ use crate::cob::store;
use crate::cob::store::{Cob, CobAction, Store, Transaction};
use crate::cob::{EntryId, ObjectId, TypeName};
use crate::crypto::ssh::ExtendedSignature;
-
use crate::crypto::Signer;
use crate::git;
+
use crate::node::device::Device;
+
use crate::node::NodeId;
use crate::prelude::ReadRepository;
use crate::storage::{Oid, WriteRepository};

@@ -315,7 +316,7 @@ impl<R> std::fmt::Debug for JobMut<'_, '_, R> {

impl<R> JobMut<'_, '_, R>
where
-
    R: WriteRepository + cob::Store,
+
    R: WriteRepository + cob::Store<Namespace = NodeId>,
{
    /// Reload the COB from storage.
    pub fn reload(&mut self) -> Result<(), store::Error> {
@@ -332,12 +333,15 @@ where

    /// Transition the `Job` into a running state, storing the provided
    /// metadata.
-
    pub fn start<G: Signer>(
+
    pub fn start<G>(
        &mut self,
        run_id: String,
        info_url: Option<String>,
-
        signer: &G,
-
    ) -> Result<EntryId, Error> {
+
        signer: &Device<G>,
+
    ) -> Result<EntryId, Error>
+
    where
+
        G: crypto::signature::Signer<crypto::Signature>,
+
    {
        self.transaction("Start", signer, |tx| {
            tx.start(run_id, info_url)?;
            Ok(())
@@ -345,18 +349,21 @@ where
    }

    /// Transition the `Job` into a finished state, with the provided `reason`.
-
    pub fn finish<G: Signer>(&mut self, reason: Reason, signer: &G) -> Result<EntryId, Error> {
+
    pub fn finish<G>(&mut self, reason: Reason, signer: &Device<G>) -> Result<EntryId, Error>
+
    where
+
        G: crypto::signature::Signer<crypto::Signature>,
+
    {
        self.transaction("Finish", signer, |tx| tx.finish(reason))
    }

    pub fn transaction<G, F>(
        &mut self,
        message: &str,
-
        signer: &G,
+
        signer: &Device<G>,
        operations: F,
    ) -> Result<EntryId, Error>
    where
-
        G: Signer,
+
        G: crypto::signature::Signer<crypto::Signature>,
        F: FnOnce(&mut Transaction<Job, R>) -> Result<(), store::Error>,
    {
        let mut tx = Transaction::default();
@@ -391,7 +398,7 @@ impl<'a, R> Deref for JobStore<'a, R> {

impl<'a, R> JobStore<'a, R>
where
-
    R: WriteRepository + ReadRepository + cob::Store,
+
    R: WriteRepository + ReadRepository + cob::Store<Namespace = NodeId>,
{
    pub fn open(repository: &'a R) -> Result<Self, store::Error> {
        let raw = store::Store::open(repository)?;
@@ -421,11 +428,14 @@ where
    }

    /// Create a fresh `Job` with the provided `commit_id`.
-
    pub fn create<'g, G: Signer>(
+
    pub fn create<'g, G>(
        &'g mut self,
        commit_id: git::Oid,
-
        signer: &G,
-
    ) -> Result<JobMut<'a, 'g, R>, Error> {
+
        signer: &Device<G>,
+
    ) -> Result<JobMut<'a, 'g, R>, Error>
+
    where
+
        G: crypto::signature::Signer<crypto::Signature>,
+
    {
        let (id, job) = Transaction::initial("Create job", &mut self.raw, signer, |tx, _| {
            tx.trigger(commit_id)?;
            Ok(())
@@ -439,7 +449,10 @@ where
    }

    /// Delete the `Job` identified by `id`.
-
    pub fn remove<G: Signer>(&self, id: &JobId, signer: &G) -> Result<(), store::Error> {
+
    pub fn remove<G>(&self, id: &JobId, signer: &Device<G>) -> Result<(), store::Error>
+
    where
+
        G: crypto::signature::Signer<crypto::Signature>,
+
    {
        self.raw.remove(id, signer)
    }
}
modified radicle/src/cob/patch.rs
@@ -21,10 +21,11 @@ use crate::cob::thread;
use crate::cob::thread::Thread;
use crate::cob::thread::{Comment, CommentId, Edit, Reactions};
use crate::cob::{op, store, ActorId, Embed, EntryId, ObjectId, TypeName, Uri};
-
use crate::crypto::{PublicKey, Signer};
+
use crate::crypto::PublicKey;
use crate::git;
use crate::identity::doc::{DocAt, DocError};
use crate::identity::PayloadError;
+
use crate::node::device::Device;
use crate::prelude::*;
use crate::storage;

@@ -347,11 +348,14 @@ impl<R: WriteRepository> Merged<'_, R> {
    ///
    /// This removes Git refs relating to the patch, both in the working copy,
    /// and the stored copy; and updates `rad/sigrefs`.
-
    pub fn cleanup<G: Signer>(
+
    pub fn cleanup<G>(
        self,
        working: &git::raw::Repository,
-
        signer: &G,
-
    ) -> Result<(), storage::RepositoryError> {
+
        signer: &Device<G>,
+
    ) -> Result<(), storage::RepositoryError>
+
    where
+
        G: crypto::signature::Signer<crypto::Signature>,
+
    {
        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);
@@ -1972,7 +1976,7 @@ pub struct PatchMut<'a, 'g, R, C> {
impl<'a, 'g, R, C> PatchMut<'a, 'g, R, C>
where
    C: cob::cache::Update<Patch>,
-
    R: ReadRepository + SignRepository + cob::Store,
+
    R: ReadRepository + SignRepository + cob::Store<Namespace = NodeId>,
{
    pub fn new(id: ObjectId, patch: Patch, cache: &'g mut Cache<Patches<'a, R>, C>) -> Self {
        Self {
@@ -2000,11 +2004,11 @@ where
    pub fn transaction<G, F>(
        &mut self,
        message: &str,
-
        signer: &G,
+
        signer: &Device<G>,
        operations: F,
    ) -> Result<EntryId, Error>
    where
-
        G: Signer,
+
        G: crypto::signature::Signer<crypto::Signature>,
        F: FnOnce(&mut Transaction<Patch, R>) -> Result<(), store::Error>,
    {
        let mut tx = Transaction::default();
@@ -2023,57 +2027,72 @@ where
    }

    /// Edit patch metadata.
-
    pub fn edit<G: Signer>(
+
    pub fn edit<G, S>(
        &mut self,
        title: String,
        target: MergeTarget,
-
        signer: &G,
-
    ) -> Result<EntryId, Error> {
+
        signer: &Device<G>,
+
    ) -> Result<EntryId, Error>
+
    where
+
        G: crypto::signature::Signer<crypto::Signature>,
+
        S: ToString,
+
    {
        self.transaction("Edit", signer, |tx| tx.edit(title, target))
    }

    /// Edit revision metadata.
-
    pub fn edit_revision<G: Signer>(
+
    pub fn edit_revision<G, S>(
        &mut self,
        revision: RevisionId,
-
        description: String,
+
        description: S,
        embeds: impl IntoIterator<Item = Embed<Uri>>,
-
        signer: &G,
-
    ) -> Result<EntryId, Error> {
+
        signer: &Device<G>,
+
    ) -> Result<EntryId, Error>
+
    where
+
        G: crypto::signature::Signer<crypto::Signature>,
+
        S: ToString,
+
    {
        self.transaction("Edit revision", signer, |tx| {
            tx.edit_revision(revision, description, embeds.into_iter().collect())
        })
    }

    /// Redact a revision.
-
    pub fn redact<G: Signer>(
-
        &mut self,
-
        revision: RevisionId,
-
        signer: &G,
-
    ) -> Result<EntryId, Error> {
+
    pub fn redact<G>(&mut self, revision: RevisionId, signer: &Device<G>) -> Result<EntryId, Error>
+
    where
+
        G: crypto::signature::Signer<crypto::Signature>,
+
    {
        self.transaction("Redact revision", signer, |tx| tx.redact(revision))
    }

    /// Create a thread on a patch revision.
-
    pub fn thread<G: Signer, S: ToString>(
+
    pub fn thread<G, S>(
        &mut self,
        revision: RevisionId,
        body: S,
-
        signer: &G,
-
    ) -> Result<CommentId, Error> {
+
        signer: &Device<G>,
+
    ) -> Result<CommentId, Error>
+
    where
+
        G: crypto::signature::Signer<crypto::Signature>,
+
        S: ToString,
+
    {
        self.transaction("Create thread", signer, |tx| tx.thread(revision, body))
    }

    /// Comment on a patch revision.
-
    pub fn comment<G: Signer, S: ToString>(
+
    pub fn comment<G, S>(
        &mut self,
        revision: RevisionId,
        body: S,
        reply_to: Option<CommentId>,
        location: Option<CodeLocation>,
        embeds: impl IntoIterator<Item = Embed<Uri>>,
-
        signer: &G,
-
    ) -> Result<EntryId, Error> {
+
        signer: &Device<G>,
+
    ) -> Result<EntryId, Error>
+
    where
+
        G: crypto::signature::Signer<crypto::Signature>,
+
        S: ToString,
+
    {
        self.transaction("Comment", signer, |tx| {
            tx.comment(
                revision,
@@ -2086,69 +2105,86 @@ where
    }

    /// React on a patch revision.
-
    pub fn react<G: Signer>(
+
    pub fn react<G>(
        &mut self,
        revision: RevisionId,
        reaction: Reaction,
        location: Option<CodeLocation>,
        active: bool,
-
        signer: &G,
-
    ) -> Result<EntryId, Error> {
+
        signer: &Device<G>,
+
    ) -> Result<EntryId, Error>
+
    where
+
        G: crypto::signature::Signer<crypto::Signature>,
+
    {
        self.transaction("React", signer, |tx| {
            tx.react(revision, reaction, location, active)
        })
    }

    /// Edit a comment on a patch revision.
-
    pub fn comment_edit<G: Signer, S: ToString>(
+
    pub fn comment_edit<G, S>(
        &mut self,
        revision: RevisionId,
        comment: CommentId,
        body: S,
        embeds: impl IntoIterator<Item = Embed<Uri>>,
-
        signer: &G,
-
    ) -> Result<EntryId, Error> {
+
        signer: &Device<G>,
+
    ) -> Result<EntryId, Error>
+
    where
+
        G: crypto::signature::Signer<crypto::Signature>,
+
        S: ToString,
+
    {
        self.transaction("Edit comment", signer, |tx| {
            tx.comment_edit(revision, comment, body, embeds.into_iter().collect())
        })
    }

    /// React to a comment on a patch revision.
-
    pub fn comment_react<G: Signer>(
+
    pub fn comment_react<G>(
        &mut self,
        revision: RevisionId,
        comment: CommentId,
        reaction: Reaction,
        active: bool,
-
        signer: &G,
-
    ) -> Result<EntryId, Error> {
+
        signer: &Device<G>,
+
    ) -> Result<EntryId, Error>
+
    where
+
        G: crypto::signature::Signer<crypto::Signature>,
+
    {
        self.transaction("React comment", signer, |tx| {
            tx.comment_react(revision, comment, reaction, active)
        })
    }

    /// Redact a comment on a patch revision.
-
    pub fn comment_redact<G: Signer>(
+
    pub fn comment_redact<G>(
        &mut self,
        revision: RevisionId,
        comment: CommentId,
-
        signer: &G,
-
    ) -> Result<EntryId, Error> {
+
        signer: &Device<G>,
+
    ) -> Result<EntryId, Error>
+
    where
+
        G: crypto::signature::Signer<crypto::Signature>,
+
    {
        self.transaction("Redact comment", signer, |tx| {
            tx.comment_redact(revision, comment)
        })
    }

    /// Comment on a line of code as part of a review.
-
    pub fn review_comment<G: Signer, S: ToString>(
+
    pub fn review_comment<G, S>(
        &mut self,
        review: ReviewId,
        body: S,
        location: Option<CodeLocation>,
        reply_to: Option<CommentId>,
        embeds: impl IntoIterator<Item = Embed<Uri>>,
-
        signer: &G,
-
    ) -> Result<EntryId, Error> {
+
        signer: &Device<G>,
+
    ) -> Result<EntryId, Error>
+
    where
+
        G: crypto::signature::Signer<crypto::Signature>,
+
        S: ToString,
+
    {
        self.transaction("Review comment", signer, |tx| {
            tx.review_comment(
                review,
@@ -2161,54 +2197,67 @@ where
    }

    /// Edit review comment.
-
    pub fn edit_review_comment<G: Signer, S: ToString>(
+
    pub fn edit_review_comment<G, S>(
        &mut self,
        review: ReviewId,
        comment: EntryId,
        body: S,
        embeds: impl IntoIterator<Item = Embed<Uri>>,
-
        signer: &G,
-
    ) -> Result<EntryId, Error> {
+
        signer: &Device<G>,
+
    ) -> Result<EntryId, Error>
+
    where
+
        G: crypto::signature::Signer<crypto::Signature>,
+
        S: ToString,
+
    {
        self.transaction("Edit review comment", signer, |tx| {
            tx.edit_review_comment(review, comment, body, embeds.into_iter().collect())
        })
    }

    /// React to a review comment.
-
    pub fn react_review_comment<G: Signer>(
+
    pub fn react_review_comment<G>(
        &mut self,
        review: ReviewId,
        comment: EntryId,
        reaction: Reaction,
        active: bool,
-
        signer: &G,
-
    ) -> Result<EntryId, Error> {
+
        signer: &Device<G>,
+
    ) -> Result<EntryId, Error>
+
    where
+
        G: crypto::signature::Signer<crypto::Signature>,
+
    {
        self.transaction("React to review comment", signer, |tx| {
            tx.react_review_comment(review, comment, reaction, active)
        })
    }

    /// React to a review comment.
-
    pub fn redact_review_comment<G: Signer>(
+
    pub fn redact_review_comment<G>(
        &mut self,
        review: ReviewId,
        comment: EntryId,
-
        signer: &G,
-
    ) -> Result<EntryId, Error> {
+
        signer: &Device<G>,
+
    ) -> Result<EntryId, Error>
+
    where
+
        G: crypto::signature::Signer<crypto::Signature>,
+
    {
        self.transaction("Redact review comment", signer, |tx| {
            tx.redact_review_comment(review, comment)
        })
    }

    /// Review a patch revision.
-
    pub fn review<G: Signer>(
+
    pub fn review<G>(
        &mut self,
        revision: RevisionId,
        verdict: Option<Verdict>,
        summary: Option<String>,
        labels: Vec<Label>,
-
        signer: &G,
-
    ) -> Result<ReviewId, Error> {
+
        signer: &Device<G>,
+
    ) -> Result<ReviewId, Error>
+
    where
+
        G: crypto::signature::Signer<crypto::Signature>,
+
    {
        if verdict.is_none() && summary.is_none() {
            return Err(Error::EmptyReview);
        }
@@ -2219,59 +2268,74 @@ where
    }

    /// Edit a review.
-
    pub fn review_edit<G: Signer>(
+
    pub fn review_edit<G>(
        &mut self,
        review: ReviewId,
        verdict: Option<Verdict>,
        summary: Option<String>,
        labels: Vec<Label>,
-
        signer: &G,
-
    ) -> Result<EntryId, Error> {
+
        signer: &Device<G>,
+
    ) -> Result<EntryId, Error>
+
    where
+
        G: crypto::signature::Signer<crypto::Signature>,
+
    {
        self.transaction("Edit review", signer, |tx| {
            tx.review_edit(review, verdict, summary, labels)
        })
    }

    /// Redact a patch review.
-
    pub fn redact_review<G: Signer>(
+
    pub fn redact_review<G>(
        &mut self,
        review: ReviewId,
-
        signer: &G,
-
    ) -> Result<EntryId, Error> {
+
        signer: &Device<G>,
+
    ) -> Result<EntryId, Error>
+
    where
+
        G: crypto::signature::Signer<crypto::Signature>,
+
    {
        self.transaction("Redact review", signer, |tx| tx.redact_review(review))
    }

    /// Resolve a patch review comment.
-
    pub fn resolve_review_comment<G: Signer>(
+
    pub fn resolve_review_comment<G>(
        &mut self,
        review: ReviewId,
        comment: CommentId,
-
        signer: &G,
-
    ) -> Result<EntryId, Error> {
+
        signer: &Device<G>,
+
    ) -> Result<EntryId, Error>
+
    where
+
        G: crypto::signature::Signer<crypto::Signature>,
+
    {
        self.transaction("Resolve review comment", signer, |tx| {
            tx.review_comment_resolve(review, comment)
        })
    }

    /// Unresolve a patch review comment.
-
    pub fn unresolve_review_comment<G: Signer>(
+
    pub fn unresolve_review_comment<G>(
        &mut self,
        review: ReviewId,
        comment: CommentId,
-
        signer: &G,
-
    ) -> Result<EntryId, Error> {
+
        signer: &Device<G>,
+
    ) -> Result<EntryId, Error>
+
    where
+
        G: crypto::signature::Signer<crypto::Signature>,
+
    {
        self.transaction("Unresolve review comment", signer, |tx| {
            tx.review_comment_unresolve(review, comment)
        })
    }

    /// Merge a patch revision.
-
    pub fn merge<G: Signer>(
+
    pub fn merge<G>(
        &mut self,
        revision: RevisionId,
        commit: git::Oid,
-
        signer: &G,
-
    ) -> Result<Merged<R>, Error> {
+
        signer: &Device<G>,
+
    ) -> Result<Merged<R>, Error>
+
    where
+
        G: crypto::signature::Signer<crypto::Signature>,
+
    {
        // TODO: Don't allow merging the same revision twice?
        let entry = self.transaction("Merge revision", signer, |tx| tx.merge(revision, commit))?;

@@ -2283,13 +2347,16 @@ where
    }

    /// Update a patch with a new revision.
-
    pub fn update<G: Signer>(
+
    pub fn update<G>(
        &mut self,
        description: impl ToString,
        base: impl Into<git::Oid>,
        oid: impl Into<git::Oid>,
-
        signer: &G,
-
    ) -> Result<RevisionId, Error> {
+
        signer: &Device<G>,
+
    ) -> Result<RevisionId, Error>
+
    where
+
        G: crypto::signature::Signer<crypto::Signature>,
+
    {
        self.transaction("Add revision", signer, |tx| {
            tx.revision(description, base, oid)
        })
@@ -2297,21 +2364,30 @@ where
    }

    /// Lifecycle a patch.
-
    pub fn lifecycle<G: Signer>(&mut self, state: Lifecycle, signer: &G) -> Result<EntryId, Error> {
+
    pub fn lifecycle<G>(&mut self, state: Lifecycle, signer: &Device<G>) -> Result<EntryId, Error>
+
    where
+
        G: crypto::signature::Signer<crypto::Signature>,
+
    {
        self.transaction("Lifecycle", signer, |tx| tx.lifecycle(state))
    }

    /// Assign a patch.
-
    pub fn assign<G: Signer>(
+
    pub fn assign<G>(
        &mut self,
        assignees: BTreeSet<Did>,
-
        signer: &G,
-
    ) -> Result<EntryId, Error> {
+
        signer: &Device<G>,
+
    ) -> Result<EntryId, Error>
+
    where
+
        G: crypto::signature::Signer<crypto::Signature>,
+
    {
        self.transaction("Assign", signer, |tx| tx.assign(assignees))
    }

    /// Archive a patch.
-
    pub fn archive<G: Signer>(&mut self, signer: &G) -> Result<bool, Error> {
+
    pub fn archive<G>(&mut self, signer: &Device<G>) -> Result<bool, Error>
+
    where
+
        G: crypto::signature::Signer<crypto::Signature>,
+
    {
        self.lifecycle(Lifecycle::Archived, signer)?;

        Ok(true)
@@ -2319,7 +2395,10 @@ where

    /// Mark an archived patch as ready to be reviewed again.
    /// Returns `false` if the patch was not archived.
-
    pub fn unarchive<G: Signer>(&mut self, signer: &G) -> Result<bool, Error> {
+
    pub fn unarchive<G>(&mut self, signer: &Device<G>) -> Result<bool, Error>
+
    where
+
        G: crypto::signature::Signer<crypto::Signature>,
+
    {
        if !self.is_archived() {
            return Ok(false);
        }
@@ -2330,7 +2409,10 @@ where

    /// Mark a patch as ready to be reviewed.
    /// Returns `false` if the patch was not a draft.
-
    pub fn ready<G: Signer>(&mut self, signer: &G) -> Result<bool, Error> {
+
    pub fn ready<G>(&mut self, signer: &Device<G>) -> Result<bool, Error>
+
    where
+
        G: crypto::signature::Signer<crypto::Signature>,
+
    {
        if !self.is_draft() {
            return Ok(false);
        }
@@ -2341,7 +2423,10 @@ where

    /// Mark an open patch as a draft.
    /// Returns `false` if the patch was not open and free of merges.
-
    pub fn unready<G: Signer>(&mut self, signer: &G) -> Result<bool, Error> {
+
    pub fn unready<G>(&mut self, signer: &Device<G>) -> Result<bool, Error>
+
    where
+
        G: crypto::signature::Signer<crypto::Signature>,
+
    {
        if !matches!(self.state(), State::Open { conflicts } if conflicts.is_empty()) {
            return Ok(false);
        }
@@ -2351,11 +2436,14 @@ where
    }

    /// Label a patch.
-
    pub fn label<G: Signer>(
+
    pub fn label<G>(
        &mut self,
        labels: impl IntoIterator<Item = Label>,
-
        signer: &G,
-
    ) -> Result<EntryId, Error> {
+
        signer: &Device<G>,
+
    ) -> Result<EntryId, Error>
+
    where
+
        G: crypto::signature::Signer<crypto::Signature>,
+
    {
        self.transaction("Label", signer, |tx| tx.label(labels))
    }
}
@@ -2419,7 +2507,7 @@ where

impl<'a, R> Patches<'a, R>
where
-
    R: ReadRepository + cob::Store,
+
    R: ReadRepository + cob::Store<Namespace = NodeId>,
{
    /// Open a patches store.
    pub fn open(repository: &'a R) -> Result<Self, RepositoryError> {
@@ -2502,7 +2590,7 @@ where

impl<'a, R> Patches<'a, R>
where
-
    R: ReadRepository + SignRepository + cob::Store,
+
    R: ReadRepository + SignRepository + cob::Store<Namespace = NodeId>,
{
    /// Open a new patch.
    pub fn create<'g, C, G>(
@@ -2514,11 +2602,11 @@ where
        oid: impl Into<git::Oid>,
        labels: &[Label],
        cache: &'g mut C,
-
        signer: &G,
+
        signer: &Device<G>,
    ) -> Result<PatchMut<'a, 'g, R, C>, Error>
    where
        C: cob::cache::Update<Patch>,
-
        G: Signer,
+
        G: crypto::signature::Signer<crypto::Signature>,
    {
        self._create(
            title,
@@ -2534,7 +2622,7 @@ where
    }

    /// Draft a patch. This patch will be created in a [`State::Draft`] state.
-
    pub fn draft<'g, C, G: Signer>(
+
    pub fn draft<'g, C, G>(
        &'g mut self,
        title: impl ToString,
        description: impl ToString,
@@ -2543,10 +2631,11 @@ where
        oid: impl Into<git::Oid>,
        labels: &[Label],
        cache: &'g mut C,
-
        signer: &G,
+
        signer: &Device<G>,
    ) -> Result<PatchMut<'a, 'g, R, C>, Error>
    where
        C: cob::cache::Update<Patch>,
+
        G: crypto::signature::Signer<crypto::Signature>,
    {
        self._create(
            title,
@@ -2581,7 +2670,7 @@ where
    }

    /// Create a patch. This is an internal function used by `create` and `draft`.
-
    fn _create<'g, C, G: Signer>(
+
    fn _create<'g, C, G>(
        &'g mut self,
        title: impl ToString,
        description: impl ToString,
@@ -2591,10 +2680,11 @@ where
        labels: &[Label],
        state: Lifecycle,
        cache: &'g mut C,
-
        signer: &G,
+
        signer: &Device<G>,
    ) -> Result<PatchMut<'a, 'g, R, C>, Error>
    where
        C: cob::cache::Update<Patch>,
+
        G: crypto::signature::Signer<crypto::Signature>,
    {
        let (id, patch) = Transaction::initial("Create patch", &mut self.raw, signer, |tx, _| {
            tx.revision(description, base, oid)?;
@@ -3070,7 +3160,7 @@ mod test {
    fn test_revision_review_merge_redacted() {
        let base = git::Oid::from_str("cb18e95ada2bb38aadd8e6cef0963ce37a87add3").unwrap();
        let oid = git::Oid::from_str("518d5069f94c03427f694bb494ac1cd7d1339380").unwrap();
-
        let mut alice = Actor::new(MockSigner::default());
+
        let mut alice = Actor::<MockSigner>::default();
        let rid = gen::<RepoId>(1);
        let doc = RawDoc::new(
            gen::<Project>(1),
@@ -3189,7 +3279,7 @@ mod test {
    fn test_revision_reaction() {
        let base = git::Oid::from_str("cb18e95ada2bb38aadd8e6cef0963ce37a87add3").unwrap();
        let oid = git::Oid::from_str("518d5069f94c03427f694bb494ac1cd7d1339380").unwrap();
-
        let mut alice = Actor::new(MockSigner::default());
+
        let mut alice = Actor::<MockSigner>::default();
        let repo = gen::<MockRepository>(1);
        let reaction = Reaction::new('👍').expect("failed to create a reaction");

modified radicle/src/cob/patch/cache.rs
@@ -9,14 +9,14 @@ use crate::cob::cache::{self, StoreReader};
use crate::cob::cache::{Remove, StoreWriter, Update};
use crate::cob::store;
use crate::cob::{Label, ObjectId, TypeName};
-
use crate::crypto::Signer;
use crate::git;
+
use crate::node::device::Device;
use crate::prelude::RepoId;
use crate::storage::{HasRepoId, ReadRepository, RepositoryError, SignRepository, WriteRepository};

use super::{
-
    ByRevision, MergeTarget, Patch, PatchCounts, PatchId, PatchMut, Revision, RevisionId, State,
-
    Status,
+
    ByRevision, MergeTarget, NodeId, Patch, PatchCounts, PatchId, PatchMut, Revision, RevisionId,
+
    State, Status,
};

/// A set of read-only methods for a [`Patch`] store.
@@ -116,11 +116,11 @@ impl<'a, R, C> Cache<super::Patches<'a, R>, C> {
        base: impl Into<git::Oid>,
        oid: impl Into<git::Oid>,
        labels: &[Label],
-
        signer: &G,
+
        signer: &Device<G>,
    ) -> Result<PatchMut<'a, 'g, R, C>, super::Error>
    where
-
        R: WriteRepository + cob::Store,
-
        G: Signer,
+
        R: WriteRepository + cob::Store<Namespace = NodeId>,
+
        G: crypto::signature::Signer<crypto::Signature>,
        C: Update<Patch>,
    {
        self.store.create(
@@ -146,11 +146,11 @@ impl<'a, R, C> Cache<super::Patches<'a, R>, C> {
        base: impl Into<git::Oid>,
        oid: impl Into<git::Oid>,
        labels: &[Label],
-
        signer: &G,
+
        signer: &Device<G>,
    ) -> Result<PatchMut<'a, 'g, R, C>, super::Error>
    where
-
        R: WriteRepository + cob::Store,
-
        G: Signer,
+
        R: WriteRepository + cob::Store<Namespace = NodeId>,
+
        G: crypto::signature::Signer<crypto::Signature>,
        C: Update<Patch>,
    {
        self.store.draft(
@@ -167,10 +167,10 @@ impl<'a, R, C> Cache<super::Patches<'a, R>, C> {

    /// Remove the given `id` from the [`super::Patches`] storage, and
    /// removing the entry from the `cache`.
-
    pub fn remove<G>(&mut self, id: &PatchId, signer: &G) -> Result<(), super::Error>
+
    pub fn remove<G>(&mut self, id: &PatchId, signer: &Device<G>) -> Result<(), super::Error>
    where
-
        G: Signer,
-
        R: ReadRepository + SignRepository + cob::Store,
+
        G: crypto::signature::Signer<crypto::Signature>,
+
        R: ReadRepository + SignRepository + cob::Store<Namespace = NodeId>,
        C: Remove<Patch>,
    {
        self.store.remove(id, signer)?;
@@ -276,7 +276,7 @@ where

impl<'a, R> Cache<super::Patches<'a, R>, cache::NoCache>
where
-
    R: ReadRepository + cob::Store,
+
    R: ReadRepository + cob::Store<Namespace = NodeId>,
{
    /// Get a `Cache` that does no write-through modifications and
    /// uses the [`super::Patches`] store for all reads and writes.
@@ -488,7 +488,7 @@ impl Iterator for NoCacheIter<'_> {

impl<R> Patches for Cache<super::Patches<'_, R>, cache::NoCache>
where
-
    R: ReadRepository + cob::Store,
+
    R: ReadRepository + cob::Store<Namespace = NodeId>,
{
    type Error = super::Error;
    type Iter<'b>
modified radicle/src/cob/store.rs
@@ -11,6 +11,7 @@ use serde::{Deserialize, Serialize};
use crate::cob::op::Op;
use crate::cob::{Create, Embed, EntryId, ObjectId, TypeName, Update, Updated, Uri, Version};
use crate::git;
+
use crate::node::device::Device;
use crate::prelude::*;
use crate::storage::git as storage;
use crate::storage::SignRepository;
@@ -173,7 +174,7 @@ where

impl<'a, T, R> Store<'a, T, R>
where
-
    R: ReadRepository + cob::Store,
+
    R: ReadRepository + cob::Store<Namespace = NodeId>,
    T: CobWithType,
{
    /// Open a new generic store.
@@ -189,7 +190,7 @@ where

impl<T, R> Store<'_, T, R>
where
-
    R: ReadRepository + cob::Store,
+
    R: ReadRepository + cob::Store<Namespace = NodeId>,
    T: Cob + cob::Evaluate<R>,
{
    pub fn transaction(
@@ -203,20 +204,23 @@ where

impl<T, R> Store<'_, T, R>
where
-
    R: ReadRepository + SignRepository + cob::Store,
+
    R: ReadRepository + SignRepository + cob::Store<Namespace = NodeId>,
    T: Cob + cob::Evaluate<R>,
    T::Action: Serialize,
{
    /// Update an object.
-
    pub fn update<G: Signer>(
+
    pub fn update<G>(
        &self,
        type_name: &TypeName,
        object_id: ObjectId,
        message: &str,
        actions: impl Into<NonEmpty<T::Action>>,
        embeds: Vec<Embed<Uri>>,
-
        signer: &G,
-
    ) -> Result<Updated<T>, Error> {
+
        signer: &Device<G>,
+
    ) -> Result<Updated<T>, Error>
+
    where
+
        G: crypto::signature::Signer<crypto::Signature>,
+
    {
        let actions = actions.into();
        let related = actions.iter().flat_map(T::Action::parents).collect();
        let changes = actions.try_map(encoding::encode)?;
@@ -251,13 +255,16 @@ where
    }

    /// Create an object.
-
    pub fn create<G: Signer>(
+
    pub fn create<G>(
        &self,
        message: &str,
        actions: impl Into<NonEmpty<T::Action>>,
        embeds: Vec<Embed<Uri>>,
-
        signer: &G,
-
    ) -> Result<(ObjectId, T), Error> {
+
        signer: &Device<G>,
+
    ) -> Result<(ObjectId, T), Error>
+
    where
+
        G: crypto::signature::Signer<crypto::Signature>,
+
    {
        let actions = actions.into();
        let parents = actions.iter().flat_map(T::Action::parents).collect();
        let contents = actions.try_map(encoding::encode)?;
@@ -270,7 +277,7 @@ where
                })
            })
            .collect::<Result<_, _>>()?;
-
        let cob = cob::create::<T, _, G>(
+
        let cob = cob::create::<T, _, _>(
            self.repo,
            signer,
            self.identity,
@@ -296,7 +303,10 @@ where
    }

    /// Remove an object.
-
    pub fn remove<G: Signer>(&self, id: &ObjectId, signer: &G) -> Result<(), Error> {
+
    pub fn remove<G>(&self, id: &ObjectId, signer: &Device<G>) -> Result<(), Error>
+
    where
+
        G: crypto::signature::Signer<crypto::Signature>,
+
    {
        let name = git::refs::storage::cob(signer.public_key(), self.type_name, id);
        match self
            .repo
@@ -405,15 +415,15 @@ where
    pub fn initial<G, F, Tx>(
        message: &str,
        store: &mut Store<T, R>,
-
        signer: &G,
+
        signer: &Device<G>,
        operations: F,
    ) -> Result<(ObjectId, T), Error>
    where
        Tx: From<Self>,
        Self: From<Tx>,
-
        G: Signer,
+
        G: crypto::signature::Signer<crypto::Signature>,
        F: FnOnce(&mut Tx, &R) -> Result<(), Error>,
-
        R: ReadRepository + SignRepository + cob::Store,
+
        R: ReadRepository + SignRepository + cob::Store<Namespace = NodeId>,
        T::Action: Serialize + Clone,
    {
        let mut tx = Tx::from(Transaction::default());
@@ -469,16 +479,17 @@ where
    /// Commit transaction.
    ///
    /// Returns an operation that can be applied onto an in-memory state.
-
    pub fn commit<G: Signer>(
+
    pub fn commit<G>(
        self,
        msg: &str,
        id: ObjectId,
        store: &mut Store<T, R>,
-
        signer: &G,
+
        signer: &Device<G>,
    ) -> Result<(T, EntryId), Error>
    where
-
        R: ReadRepository + SignRepository + cob::Store,
+
        R: ReadRepository + SignRepository + cob::Store<Namespace = NodeId>,
        T::Action: Serialize + Clone,
+
        G: crypto::signature::Signer<crypto::Signature>,
    {
        let actions = NonEmpty::from_vec(self.actions)
            .expect("Transaction::commit: transaction must not be empty");
modified radicle/src/cob/test.rs
@@ -17,6 +17,7 @@ use crate::git::ext::author::Author;
use crate::git::ext::commit::headers::Headers;
use crate::git::ext::commit::{trailers::OwnedTrailer, Commit};
use crate::git::Oid;
+
use crate::node::device::Device;
use crate::prelude::Did;
use crate::profile::env;
use crate::storage::ReadRepository;
@@ -148,17 +149,17 @@ where

/// An object that can be used to create and sign operations.
pub struct Actor<G> {
-
    pub signer: G,
+
    pub signer: Device<G>,
}

-
impl<G: Default> Default for Actor<G> {
+
impl<G: Default + Signer> Default for Actor<G> {
    fn default() -> Self {
-
        Self::new(G::default())
+
        Self::new(Device::default())
    }
}

impl<G> Actor<G> {
-
    pub fn new(signer: G) -> Self {
+
    pub fn new(signer: Device<G>) -> Self {
        Self { signer }
    }
}
modified radicle/src/cob/thread.rs
@@ -638,6 +638,7 @@ mod tests {
    use crate::cob::test;
    use crate::crypto::test::signer::MockSigner;
    use crate::crypto::Signer;
+
    use crate::node::device::Device;
    use crate::profile::env;
    use crate::test::arbitrary;
    use crate::test::arbitrary::gen;
@@ -651,13 +652,13 @@ mod tests {
    impl<G: Default + Signer> Default for Actor<G> {
        fn default() -> Self {
            Self {
-
                inner: cob::test::Actor::new(G::default()),
+
                inner: cob::test::Actor::<G>::default(),
            }
        }
    }

    impl<G: Signer> Actor<G> {
-
        pub fn new(signer: G) -> Self {
+
        pub fn new(signer: Device<G>) -> Self {
            Self {
                inner: cob::test::Actor::new(signer),
            }
modified radicle/src/git/canonical.rs
@@ -249,12 +249,11 @@ impl Canonical {
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
-
    use crypto::test::signer::MockSigner;
-
    use radicle_crypto::Signer;

    use super::*;
    use crate::assert_matches;
    use crate::git;
+
    use crate::node::device::Device;
    use crate::test::fixtures;

    /// Test helper to construct a Canonical and get the quorum
@@ -267,7 +266,7 @@ mod tests {
            .iter()
            .enumerate()
            .map(|(i, head)| {
-
                let signer = MockSigner::from_seed([(i + 1) as u8; 32]);
+
                let signer = Device::mock_from_seed([(i + 1) as u8; 32]);
                let did = Did::from(signer.public_key());
                (did, (*head).into())
            })
modified radicle/src/identity/doc.rs
@@ -20,6 +20,7 @@ use crate::crypto;
use crate::crypto::Signature;
use crate::git;
use crate::identity::{project::Project, Did};
+
use crate::node::device::Device;
use crate::storage;
use crate::storage::{ReadRepository, RepositoryError};

@@ -803,10 +804,10 @@ impl Doc {

    /// [`Doc::encode`] and sign the [`Doc`], returning the set of bytes, its
    /// corresponding Git [`Oid`] and the [`Signature`] over the [`Oid`].
-
    pub fn sign<G: crypto::Signer>(
-
        &self,
-
        signer: &G,
-
    ) -> Result<(git::Oid, Vec<u8>, Signature), DocError> {
+
    pub fn sign<G>(&self, signer: &G) -> Result<(git::Oid, Vec<u8>, Signature), DocError>
+
    where
+
        G: crypto::signature::Signer<crypto::Signature>,
+
    {
        let (oid, bytes) = self.encode()?;
        let sig = signer.sign(oid.as_bytes());

@@ -814,7 +815,10 @@ impl Doc {
    }

    /// Similar to [`Doc::sign`], but only returning the [`Signature`].
-
    pub fn signature_of<G: crypto::Signer>(&self, signer: &G) -> Result<Signature, DocError> {
+
    pub fn signature_of<G>(&self, signer: &G) -> Result<Signature, DocError>
+
    where
+
        G: crypto::signature::Signer<crypto::Signature>,
+
    {
        let (_, _, sig) = self.sign(signer)?;

        Ok(sig)
@@ -835,11 +839,14 @@ impl Doc {

    /// Initialize an [`identity::Identity`] with this [`Doc`] as the associated
    /// document.
-
    pub fn init<G: crypto::Signer>(
+
    pub fn init<G>(
        &self,
        repo: &storage::git::Repository,
-
        signer: &G,
-
    ) -> Result<git::Oid, RepositoryError> {
+
        signer: &Device<G>,
+
    ) -> Result<git::Oid, RepositoryError>
+
    where
+
        G: crypto::signature::Signer<crypto::Signature>,
+
    {
        let cob = identity::Identity::initialize(self, repo, signer)?;
        let id_ref = git::refs::storage::id(signer.public_key());
        let cob_ref = git::refs::storage::cob(
@@ -862,8 +869,6 @@ impl Doc {
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod test {
-
    use radicle_crypto::test::signer::MockSigner;
-
    use radicle_crypto::Signer as _;
    use serde_json::json;

    use crate::assert_matches;
@@ -880,7 +885,7 @@ mod test {

    #[test]
    fn test_duplicate_dids() {
-
        let delegate = MockSigner::from_seed([0xff; 32]);
+
        let delegate = Device::mock_from_seed([0xff; 32]);
        let did = Did::from(delegate.public_key());
        let mut doc = RawDoc::new(gen::<Project>(1), vec![did], 1, Visibility::Public);
        doc.delegate(did);
@@ -1015,7 +1020,7 @@ mod test {

        transport::local::register(storage.clone());

-
        let delegate = MockSigner::from_seed([0xff; 32]);
+
        let delegate = Device::mock_from_seed([0xff; 32]);
        let (repo, _) = fixtures::repository(tempdir.path().join("working"));
        let (id, _, _) = rad::init(
            &repo,
@@ -1063,7 +1068,7 @@ mod test {

        let (working, _) = fixtures::repository(tempdir.path().join("working"));

-
        let delegate = MockSigner::from_seed([0xff; 32]);
+
        let delegate = Device::mock_from_seed([0xff; 32]);
        let (rid, doc, _) = rad::init(
            &working,
            "heartwood".try_into().unwrap(),
modified radicle/src/lib.rs
@@ -39,7 +39,7 @@ pub use storage::git::Storage;
pub mod prelude {
    use super::*;

-
    pub use crypto::{PublicKey, Signer, Verified};
+
    pub use crypto::{PublicKey, Verified};
    pub use identity::{project::Project, Did, Doc, RawDoc, RepoId};
    pub use node::{Alias, NodeId, Timestamp};
    pub use profile::Profile;
modified radicle/src/node.rs
@@ -5,6 +5,7 @@ mod features;
pub mod address;
pub mod config;
pub mod db;
+
pub mod device;
pub mod events;
pub mod notifications;
pub mod policy;
added radicle/src/node/device.rs
@@ -0,0 +1,174 @@
+
use std::fmt;
+
use std::ops::Deref;
+

+
use crypto::{
+
    signature::{Signer, Verifier},
+
    ssh::ExtendedSignature,
+
    Signature,
+
};
+

+
use crate::crypto;
+

+
use super::NodeId;
+

+
/// A `Device` identifies the local node through its [`NodeId`], and carries its
+
/// signing mechanism.
+
///
+
/// The signing mechanism is for node specific cryptography, e.g. signing
+
/// `rad/sigrefs`, COB commits, node messages, etc.
+
///
+
/// Note that a `Device` can create [`Signature`]s and [`ExtendedSignature`]s.
+
/// It can achieve this as long as `S` implements `Signer<Signature>`.
+
#[derive(Clone)]
+
pub struct Device<S> {
+
    node: NodeId,
+
    signer: S,
+
}
+

+
impl<S> fmt::Debug for Device<S> {
+
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+
        f.debug_struct("Device")
+
            .field("node", &self.node.to_human())
+
            .finish()
+
    }
+
}
+

+
impl<S: crypto::Signer + Default> Default for Device<S> {
+
    fn default() -> Self {
+
        Self::from(S::default())
+
    }
+
}
+

+
impl<S> Device<S> {
+
    /// Construct a new `Device`.
+
    pub fn new(node: NodeId, signer: S) -> Self {
+
        Self { node, signer }
+
    }
+

+
    /// Return the [`NodeId`] of the `Device.`
+
    pub fn node_id(&self) -> &NodeId {
+
        &self.node
+
    }
+

+
    /// Return the [`crypto::PublicKey`] of the `Device.`
+
    pub fn public_key(&self) -> &crypto::PublicKey {
+
        &self.node
+
    }
+

+
    /// Convert the `Device` into its signer.
+
    ///
+
    /// This consumes the `Device`.
+
    pub fn into_inner(self) -> S {
+
        self.signer
+
    }
+
}
+

+
#[cfg(any(test, feature = "test"))]
+
impl Device<crypto::test::signer::MockSigner> {
+
    /// Construct a `Device` using a default `MockSigner` as the device signer.
+
    pub fn mock() -> Self {
+
        Device::from(crypto::test::signer::MockSigner::default())
+
    }
+

+
    /// Construct a `Device`, constructing an RNG'd `MockSigner` for the signer.
+
    pub fn mock_rng(rng: &mut fastrand::Rng) -> Self {
+
        Device::from(crypto::test::signer::MockSigner::new(rng))
+
    }
+

+
    /// Construct a `Device`, constructing a seeded `MockSigner` for the signer.
+
    pub fn mock_from_seed(seed: [u8; 32]) -> Self {
+
        Device::from(crypto::test::signer::MockSigner::from_seed(seed))
+
    }
+
}
+

+
impl<S: Signer<Signature> + 'static> Device<S> {
+
    /// Construct a [`BoxedDevice`] from a given `Device`.
+
    pub fn boxed(self) -> BoxedDevice {
+
        BoxedDevice(Device {
+
            node: self.node,
+
            signer: BoxedSigner(Box::new(self.signer)),
+
        })
+
    }
+
}
+

+
impl<S> Verifier<Signature> for Device<S> {
+
    fn verify(&self, msg: &[u8], signature: &Signature) -> Result<(), crypto::signature::Error> {
+
        self.node
+
            .verify(msg, signature)
+
            .map_err(crypto::signature::Error::from_source)
+
    }
+
}
+

+
impl<S: crypto::Signer> From<S> for Device<S> {
+
    fn from(signer: S) -> Self {
+
        Self {
+
            node: *signer.public_key(),
+
            signer,
+
        }
+
    }
+
}
+

+
impl<S: crypto::Signer + Clone> From<&S> for Device<S> {
+
    fn from(signer: &S) -> Self {
+
        Self::from(signer.clone())
+
    }
+
}
+

+
impl<S: Signer<Signature>> Signer<Signature> for Device<S> {
+
    fn try_sign(&self, msg: &[u8]) -> Result<Signature, crypto::signature::Error> {
+
        self.signer.try_sign(msg)
+
    }
+
}
+

+
impl<S: Signer<Signature>> Signer<ExtendedSignature> for Device<S> {
+
    fn try_sign(&self, msg: &[u8]) -> Result<ExtendedSignature, crypto::signature::Error> {
+
        Ok(ExtendedSignature {
+
            key: *self.public_key(),
+
            sig: self.signer.try_sign(msg)?,
+
        })
+
    }
+
}
+

+
/// A `Signer<Signature>` that is packed in a [`Box`] for dynamic dispatch.
+
pub struct BoxedSigner(Box<dyn Signer<Signature> + 'static>);
+

+
impl Signer<Signature> for BoxedSigner {
+
    fn try_sign(&self, msg: &[u8]) -> Result<Signature, crypto::signature::Error> {
+
        self.0.try_sign(msg)
+
    }
+
}
+

+
/// A `Device` where the signer is a dynamic `Signer<Signature>`, in the form of
+
/// a [`BoxedSigner`].
+
///
+
/// This can be constructed via [`Device::boxed`].
+
pub struct BoxedDevice(Device<BoxedSigner>);
+

+
impl AsRef<Device<BoxedSigner>> for BoxedDevice {
+
    fn as_ref(&self) -> &Device<BoxedSigner> {
+
        &self.0
+
    }
+
}
+

+
impl Deref for BoxedDevice {
+
    type Target = Device<BoxedSigner>;
+

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

+
impl Signer<Signature> for BoxedDevice {
+
    fn try_sign(&self, msg: &[u8]) -> Result<Signature, crypto::signature::Error> {
+
        self.0.signer.try_sign(msg)
+
    }
+
}
+

+
impl Signer<ExtendedSignature> for BoxedDevice {
+
    fn try_sign(&self, msg: &[u8]) -> Result<ExtendedSignature, crypto::signature::Error> {
+
        Ok(ExtendedSignature {
+
            key: *self.0.public_key(),
+
            sig: self.0.signer.try_sign(msg)?,
+
        })
+
    }
+
}
modified radicle/src/profile.rs
@@ -24,7 +24,8 @@ use thiserror::Error;
use crate::cob::migrate;
use crate::crypto::ssh::agent::Agent;
use crate::crypto::ssh::{keystore, Keystore, Passphrase};
-
use crate::crypto::{PublicKey, Signer};
+
use crate::crypto::PublicKey;
+
use crate::node::device::{BoxedDevice, Device};
use crate::node::policy::config::store::Read;
use crate::node::{
    notifications, policy, policy::Scope, Alias, AliasStore, Handle as _, Node, UserAgent,
@@ -293,22 +294,22 @@ impl Profile {
        Did::from(self.public_key)
    }

-
    pub fn signer(&self) -> Result<Box<dyn Signer>, Error> {
+
    pub fn signer(&self) -> Result<BoxedDevice, Error> {
        if !self.keystore.is_encrypted()? {
            let signer = keystore::MemorySigner::load(&self.keystore, None)?;
-
            return Ok(signer.boxed());
+
            return Ok(Device::from(signer).boxed());
        }

        if let Some(passphrase) = env::passphrase() {
            let signer = keystore::MemorySigner::load(&self.keystore, Some(passphrase))?;
-
            return Ok(signer.boxed());
+
            return Ok(Device::from(signer).boxed());
        }

        match Agent::connect() {
            Ok(agent) => {
                let signer = agent.signer(self.public_key);
                if signer.is_ready()? {
-
                    Ok(signer.boxed())
+
                    Ok(Device::from(signer).boxed())
                } else {
                    Err(Error::KeyNotRegistered(self.public_key))
                }
@@ -604,7 +605,7 @@ impl Home {
        repository: &'a R,
    ) -> Result<cob::issue::Cache<cob::issue::Issues<'a, R>, cob::cache::StoreReader>, Error>
    where
-
        R: ReadRepository + cob::Store,
+
        R: ReadRepository + cob::Store<Namespace = NodeId>,
    {
        let db = self.cobs_db()?;
        let store = cob::issue::Issues::open(repository)?;
@@ -620,7 +621,7 @@ impl Home {
        repository: &'a R,
    ) -> Result<cob::issue::Cache<cob::issue::Issues<'a, R>, cob::cache::StoreWriter>, Error>
    where
-
        R: ReadRepository + cob::Store,
+
        R: ReadRepository + cob::Store<Namespace = NodeId>,
    {
        let db = self.cobs_db_mut()?;
        let store = cob::issue::Issues::open(repository)?;
@@ -636,7 +637,7 @@ impl Home {
        repository: &'a R,
    ) -> Result<cob::patch::Cache<cob::patch::Patches<'a, R>, cob::cache::StoreReader>, Error>
    where
-
        R: ReadRepository + cob::Store,
+
        R: ReadRepository + cob::Store<Namespace = NodeId>,
    {
        let db = self.cobs_db()?;
        let store = cob::patch::Patches::open(repository)?;
@@ -652,7 +653,7 @@ impl Home {
        repository: &'a R,
    ) -> Result<cob::patch::Cache<cob::patch::Patches<'a, R>, cob::cache::StoreWriter>, Error>
    where
-
        R: ReadRepository + cob::Store,
+
        R: ReadRepository + cob::Store<Namespace = NodeId>,
    {
        let db = self.cobs_db_mut()?;
        let store = cob::patch::Patches::open(repository)?;
modified radicle/src/rad.rs
@@ -7,11 +7,12 @@ use once_cell::sync::Lazy;
use thiserror::Error;

use crate::cob::ObjectId;
-
use crate::crypto::{Signer, Verified};
+
use crate::crypto::Verified;
use crate::git;
use crate::identity::doc;
use crate::identity::doc::{DocError, RepoId, Visibility};
use crate::identity::project::{Project, ProjectName};
+
use crate::node::device::Device;
use crate::storage::git::transport;
use crate::storage::git::Repository;
use crate::storage::refs::SignedRefs;
@@ -48,15 +49,19 @@ pub enum InitError {
}

/// Initialize a new radicle project from a git repository.
-
pub fn init<G: Signer, S: WriteStorage>(
+
pub fn init<G, S>(
    repo: &git2::Repository,
    name: ProjectName,
    description: &str,
    default_branch: BranchName,
    visibility: Visibility,
-
    signer: &G,
+
    signer: &Device<G>,
    storage: S,
-
) -> Result<(RepoId, identity::Doc, SignedRefs<Verified>), InitError> {
+
) -> Result<(RepoId, identity::Doc, SignedRefs<Verified>), InitError>
+
where
+
    G: crypto::signature::Signer<crypto::Signature>,
+
    S: WriteStorage,
+
{
    // TODO: Better error when project id already exists in storage, but remote doesn't.
    let delegate: identity::Did = signer.public_key().into();
    let proj = Project::new(
@@ -98,10 +103,10 @@ fn init_configure<G>(
    default_branch: &BranchName,
    url: &git::Url,
    identity: git::Oid,
-
    signer: &G,
+
    signer: &Device<G>,
) -> Result<SignedRefs<Verified>, InitError>
where
-
    G: crypto::Signer,
+
    G: crypto::signature::Signer<crypto::Signature>,
{
    let pk = signer.public_key();

@@ -162,12 +167,16 @@ pub enum ForkError {
}

/// Create a local tree for an existing project, from an existing remote.
-
pub fn fork_remote<G: Signer, S: storage::WriteStorage>(
+
pub fn fork_remote<G, S>(
    proj: RepoId,
    remote: &RemoteId,
-
    signer: &G,
+
    signer: &Device<G>,
    storage: S,
-
) -> Result<(), ForkError> {
+
) -> Result<(), ForkError>
+
where
+
    G: crypto::signature::Signer<crypto::Signature>,
+
    S: storage::WriteStorage,
+
{
    // TODO: Copy tags over?

    // Creates or copies the following references:
@@ -197,11 +206,11 @@ pub fn fork_remote<G: Signer, S: storage::WriteStorage>(
    Ok(())
}

-
pub fn fork<G: Signer, S: storage::WriteStorage>(
-
    rid: RepoId,
-
    signer: &G,
-
    storage: &S,
-
) -> Result<(), ForkError> {
+
pub fn fork<G, S>(rid: RepoId, signer: &Device<G>, storage: &S) -> Result<(), ForkError>
+
where
+
    G: crypto::signature::Signer<crypto::Signature>,
+
    S: storage::WriteStorage,
+
{
    let me = signer.public_key();
    let repository = storage.repository_mut(rid)?;
    let (canonical_branch, canonical_head) = repository.head()?;
@@ -451,7 +460,6 @@ mod tests {
    use std::collections::HashMap;

    use pretty_assertions::assert_eq;
-
    use radicle_crypto::test::signer::MockSigner;

    use crate::git::{name::component, qualified};
    use crate::identity::Did;
@@ -465,7 +473,7 @@ mod tests {
    #[test]
    fn test_init() {
        let tempdir = tempfile::tempdir().unwrap();
-
        let signer = MockSigner::default();
+
        let signer = Device::mock();
        let public_key = *signer.public_key();
        let storage = Storage::open(tempdir.path().join("storage"), fixtures::user()).unwrap();

@@ -518,8 +526,8 @@ mod tests {
    fn test_fork() {
        let mut rng = fastrand::Rng::new();
        let tempdir = tempfile::tempdir().unwrap();
-
        let alice = MockSigner::new(&mut rng);
-
        let bob = MockSigner::new(&mut rng);
+
        let alice = Device::mock_rng(&mut rng);
+
        let bob = Device::mock_rng(&mut rng);
        let bob_id = bob.public_key();
        let storage = Storage::open(tempdir.path().join("storage"), fixtures::user()).unwrap();

@@ -556,7 +564,7 @@ mod tests {
    #[test]
    fn test_checkout() {
        let tempdir = tempfile::tempdir().unwrap();
-
        let signer = MockSigner::default();
+
        let signer = Device::mock();
        let remote_id = signer.public_key();
        let storage = Storage::open(tempdir.path().join("storage"), fixtures::user()).unwrap();

modified radicle/src/storage.rs
@@ -10,7 +10,7 @@ use nonempty::NonEmpty;
use serde::{Deserialize, Serialize};
use thiserror::Error;

-
use crypto::{PublicKey, Signer, Unverified, Verified};
+
use crypto::{PublicKey, Unverified, Verified};
pub use git::{Validation, Validations};
pub use radicle_git_ext::Oid;

@@ -21,6 +21,7 @@ use crate::git::{refspec::Refspec, PatternString, Qualified, RefError, RefStr, R
use crate::identity::{Did, PayloadError};
use crate::identity::{Doc, DocAt, DocError};
use crate::identity::{Identity, RepoId};
+
use crate::node::device::Device;
use crate::node::SyncedAt;
use crate::storage::git::NAMESPACES_GLOB;
use crate::storage::refs::Refs;
@@ -657,7 +658,9 @@ 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>, RepositoryError>;
+
    fn sign_refs<G>(&self, signer: &Device<G>) -> Result<SignedRefs<Verified>, RepositoryError>
+
    where
+
        G: crypto::signature::Signer<crypto::Signature>;
}

impl<T, S> ReadStorage for T
modified radicle/src/storage/git.rs
@@ -7,7 +7,7 @@ use std::ops::{Deref, DerefMut};
use std::path::{Path, PathBuf};
use std::{fs, io};

-
use crypto::{Signer, Verified};
+
use crypto::Verified;
use once_cell::sync::Lazy;
use tempfile::TempDir;

@@ -15,6 +15,7 @@ use crate::git::canonical::Canonical;
use crate::identity::doc::DocError;
use crate::identity::{Doc, DocAt, RepoId};
use crate::identity::{Identity, Project};
+
use crate::node::device::Device;
use crate::node::SyncedAt;
use crate::storage::refs;
use crate::storage::refs::{Refs, SignedRefs, SignedRefsAt};
@@ -433,11 +434,15 @@ impl Repository {
    }

    /// Create the repository's identity branch.
-
    pub fn init<G: Signer, S: WriteStorage>(
+
    pub fn init<G, S>(
        doc: &Doc,
        storage: &S,
-
        signer: &G,
-
    ) -> Result<(Self, git::Oid), RepositoryError> {
+
        signer: &Device<G>,
+
    ) -> Result<(Self, git::Oid), RepositoryError>
+
    where
+
        G: crypto::signature::Signer<crypto::Signature>,
+
        S: WriteStorage,
+
    {
        let (doc_oid, doc_bytes) = doc.encode()?;
        let id = RepoId::from(doc_oid);
        let repo = Self::create(paths::repository(storage, &id), id, storage.info())?;
@@ -885,7 +890,10 @@ impl WriteRepository for Repository {
}

impl SignRepository for Repository {
-
    fn sign_refs<G: Signer>(&self, signer: &G) -> Result<SignedRefs<Verified>, RepositoryError> {
+
    fn sign_refs<G: crypto::signature::Signer<crypto::Signature>>(
+
        &self,
+
        signer: &Device<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() {
@@ -959,7 +967,6 @@ pub mod paths {
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
-
    use crypto::test::signer::MockSigner;

    use super::*;
    use crate::git;
@@ -970,7 +977,7 @@ mod tests {
    #[test]
    fn test_remote_refs() {
        let dir = tempfile::tempdir().unwrap();
-
        let signer = MockSigner::default();
+
        let signer = Device::mock();
        let storage = fixtures::storage(dir.path(), &signer).unwrap();
        let inv = storage.repositories().unwrap();
        let proj = inv.first().unwrap();
@@ -996,7 +1003,7 @@ mod tests {
    #[test]
    fn test_references_of() {
        let tmp = tempfile::tempdir().unwrap();
-
        let signer = MockSigner::default();
+
        let signer = Device::mock();
        let storage = Storage::open(tmp.path().join("storage"), fixtures::user()).unwrap();

        transport::local::register(storage.clone());
@@ -1031,7 +1038,7 @@ mod tests {
    fn test_sign_refs() {
        let tmp = tempfile::tempdir().unwrap();
        let mut rng = fastrand::Rng::new();
-
        let signer = MockSigner::new(&mut rng);
+
        let signer = Device::mock_rng(&mut rng);
        let storage = Storage::open(tmp.path(), fixtures::user()).unwrap();
        let alice = *signer.public_key();
        let (rid, _, working, _) =
modified radicle/src/storage/git/cob.rs
@@ -3,6 +3,7 @@ use std::collections::BTreeMap;
use std::path::Path;

use cob::object::Objects;
+
use cob::signatures::ExtendedSignature;
use radicle_cob as cob;
use radicle_cob::change;
use storage::RemoteRepository;
@@ -10,17 +11,18 @@ use storage::RepositoryError;
use storage::SignRepository;
use storage::ValidateRepository;

+
use crate::git;
use crate::git::*;
+
use crate::identity;
+
use crate::identity::doc::DocError;
+
use crate::node::device::Device;
+
use crate::node::NodeId;
use crate::storage;
use crate::storage::Error;
use crate::storage::{
    git::{Remote, Remotes, Validations},
    ReadRepository, Verified,
};
-
use crate::{
-
    git, identity,
-
    identity::{doc::DocError, PublicKey},
-
};

use super::{RemoteId, Repository};

@@ -68,7 +70,7 @@ impl change::Storage for Repository {
        spec: change::Template<Self::ObjectId>,
    ) -> Result<cob::Entry, Self::StoreError>
    where
-
        Signer: crypto::Signer,
+
        Signer: crypto::signature::Signer<ExtendedSignature>,
    {
        self.backend.store(authority, parents, signer, spec)
    }
@@ -88,6 +90,8 @@ impl cob::object::Storage for Repository {
    type UpdateError = git2::Error;
    type RemoveError = git2::Error;

+
    type Namespace = NodeId;
+

    fn objects(
        &self,
        typename: &cob::TypeName,
@@ -142,13 +146,13 @@ impl cob::object::Storage for Repository {

    fn update(
        &self,
-
        identifier: &PublicKey,
+
        namespace: &Self::Namespace,
        typename: &cob::TypeName,
        object_id: &cob::ObjectId,
        entry: &cob::EntryId,
    ) -> Result<(), Self::UpdateError> {
        self.backend.reference(
-
            git::refs::storage::cob(identifier, typename, object_id).as_str(),
+
            git::refs::storage::cob(namespace, typename, object_id).as_str(),
            (*entry).into(),
            true,
            &format!(
@@ -162,13 +166,13 @@ impl cob::object::Storage for Repository {

    fn remove(
        &self,
-
        identifier: &PublicKey,
+
        namespace: &Self::Namespace,
        typename: &cob::TypeName,
        object_id: &cob::ObjectId,
    ) -> Result<(), Self::RemoveError> {
        let mut reference = self
            .backend
-
            .find_reference(git::refs::storage::cob(identifier, typename, object_id).as_str())?;
+
            .find_reference(git::refs::storage::cob(namespace, typename, object_id).as_str())?;

        reference.delete()
    }
@@ -208,7 +212,7 @@ impl<R: storage::WriteRepository> change::Storage for DraftStore<'_, R> {
        spec: change::Template<Self::ObjectId>,
    ) -> Result<cob::Entry, Self::StoreError>
    where
-
        Signer: crypto::Signer,
+
        Signer: crypto::signature::Signer<ExtendedSignature>,
    {
        self.repo.raw().store(authority, parents, signer, spec)
    }
@@ -222,10 +226,13 @@ impl<R: storage::WriteRepository> change::Storage for DraftStore<'_, R> {
    }
}

-
impl<R: storage::ReadRepository> SignRepository for DraftStore<'_, R> {
-
    fn sign_refs<G: crypto::Signer>(
+
impl<R> SignRepository for DraftStore<'_, R>
+
where
+
    R: storage::ReadRepository,
+
{
+
    fn sign_refs<G: crypto::signature::Signer<crypto::Signature>>(
        &self,
-
        signer: &G,
+
        signer: &Device<G>,
    ) -> 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.
@@ -370,6 +377,8 @@ impl<R: storage::WriteRepository> cob::object::Storage for DraftStore<'_, R> {
    type UpdateError = git2::Error;
    type RemoveError = git2::Error;

+
    type Namespace = NodeId;
+

    fn objects(
        &self,
        typename: &cob::TypeName,
@@ -414,13 +423,13 @@ impl<R: storage::WriteRepository> cob::object::Storage for DraftStore<'_, R> {

    fn update(
        &self,
-
        identifier: &PublicKey,
+
        namespace: &Self::Namespace,
        typename: &cob::TypeName,
        object_id: &cob::ObjectId,
        entry: &cob::history::EntryId,
    ) -> Result<(), Self::UpdateError> {
        self.repo.raw().reference(
-
            git::refs::storage::draft::cob(identifier, typename, object_id).as_str(),
+
            git::refs::storage::draft::cob(namespace, typename, object_id).as_str(),
            (*entry).into(),
            true,
            &format!(
@@ -434,12 +443,12 @@ impl<R: storage::WriteRepository> cob::object::Storage for DraftStore<'_, R> {

    fn remove(
        &self,
-
        identifier: &PublicKey,
+
        namespace: &Self::Namespace,
        typename: &cob::TypeName,
        object_id: &cob::ObjectId,
    ) -> Result<(), Self::RemoveError> {
        let mut reference = self.repo.raw().find_reference(
-
            git::refs::storage::draft::cob(identifier, typename, object_id).as_str(),
+
            git::refs::storage::draft::cob(namespace, typename, object_id).as_str(),
        )?;

        reference.delete()
modified radicle/src/storage/refs.rs
@@ -7,13 +7,15 @@ use std::ops::{Deref, DerefMut};
use std::path::Path;
use std::str::FromStr;

-
use crypto::{PublicKey, Signature, Signer, SignerError, Unverified, Verified};
+
use crypto::signature::Signer;
+
use crypto::{PublicKey, Signature, Unverified, Verified};
use serde::{Deserialize, Serialize};
use thiserror::Error;

use crate::git;
use crate::git::ext as git_ext;
use crate::git::Oid;
+
use crate::node::device::Device;
use crate::profile::env;
use crate::storage;
use crate::storage::{ReadRepository, RemoteId, RepoId, WriteRepository};
@@ -39,7 +41,7 @@ pub enum Error {
    #[error("invalid signature: {0}")]
    InvalidSignature(#[from] crypto::Error),
    #[error("signer error: {0}")]
-
    Signer(#[from] SignerError),
+
    Signer(#[from] crypto::signature::Error),
    #[error("canonical refs: {0}")]
    Canonical(#[from] canonical::Error),
    #[error("invalid reference")]
@@ -88,15 +90,15 @@ impl Refs {
    }

    /// Sign these refs with the given signer and return [`SignedRefs`].
-
    pub fn signed<G>(self, signer: &G) -> Result<SignedRefs<Unverified>, Error>
+
    pub fn signed<G>(self, device: &Device<G>) -> Result<SignedRefs<Unverified>, Error>
    where
-
        G: Signer,
+
        G: Signer<crypto::Signature>,
    {
        let refs = self;
        let msg = refs.canonical();
-
        let signature = signer.try_sign(&msg)?;
+
        let signature = device.try_sign(&msg)?;

-
        Ok(SignedRefs::new(refs, *signer.public_key(), signature))
+
        Ok(SignedRefs::new(refs, *device.public_key(), signature))
    }

    /// Get a particular ref.
@@ -464,7 +466,6 @@ pub mod canonical {
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
-
    use crypto::test::signer::MockSigner;
    use qcheck_macros::quickcheck;
    use storage::{git::transport, RemoteRepository, SignRepository, WriteStorage};

@@ -490,8 +491,8 @@ mod tests {
    // `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 alice = Device::mock();
+
        let bob = Device::mock();
        let storage = &Storage::open(tmp.path().join("storage"), fixtures::user()).unwrap();

        transport::local::register(storage.clone());
modified radicle/src/test.rs
@@ -74,6 +74,7 @@ pub mod setup {

    use super::storage::{Namespaces, RefUpdate};
    use crate::crypto::test::signer::MockSigner;
+
    use crate::node::device::Device;
    use crate::storage::git::transport::remote;
    use crate::{
        git,
@@ -91,7 +92,7 @@ pub mod setup {
        pub tmp: TempDir,
        pub root: PathBuf,
        pub storage: Storage,
-
        pub signer: MockSigner,
+
        pub signer: Device<MockSigner>,
    }

    impl Default for Node {
@@ -104,6 +105,7 @@ pub mod setup {

    impl Node {
        pub fn new(tmp: TempDir, signer: MockSigner, alias: &str) -> Self {
+
            let signer = Device::from(signer);
            let root = tmp.path().to_path_buf();
            let home = root.join("home");
            let paths = Home::new(home.as_path()).unwrap();
modified radicle/src/test/arbitrary.rs
@@ -245,7 +245,7 @@ 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();
+
        let signed = refs.signed(&signer.into()).unwrap();

        storage::Remote::<crypto::Unverified>::new(signed)
    }
modified radicle/src/test/fixtures.rs
@@ -1,10 +1,11 @@
use std::path::Path;
use std::str::FromStr;

-
use crate::crypto::{PublicKey, Signer, Verified};
+
use crate::crypto::{PublicKey, Verified};
use crate::git;
use crate::identity::doc::Visibility;
use crate::identity::RepoId;
+
use crate::node::device::Device;
use crate::node::Alias;
use crate::rad;
use crate::storage::git::transport;
@@ -23,7 +24,11 @@ pub fn user() -> git::UserInfo {
}

/// Create a new storage with a project.
-
pub fn storage<P: AsRef<Path>, G: Signer>(path: P, signer: &G) -> Result<Storage, rad::InitError> {
+
pub fn storage<P, G>(path: P, signer: &Device<G>) -> Result<Storage, rad::InitError>
+
where
+
    P: AsRef<Path>,
+
    G: crypto::signature::Signer<crypto::Signature>,
+
{
    let path = path.as_ref();
    let storage = Storage::open(
        path.join("storage"),
@@ -57,11 +62,15 @@ pub fn storage<P: AsRef<Path>, G: Signer>(path: P, signer: &G) -> Result<Storage
}

/// Create a new repository at the given path, and initialize it into a project.
-
pub fn project<P: AsRef<Path>, G: Signer>(
+
pub fn project<P, G>(
    path: P,
    storage: &Storage,
-
    signer: &G,
-
) -> Result<(RepoId, SignedRefs<Verified>, git2::Repository, git2::Oid), rad::InitError> {
+
    signer: &Device<G>,
+
) -> Result<(RepoId, SignedRefs<Verified>, git2::Repository, git2::Oid), rad::InitError>
+
where
+
    P: AsRef<Path>,
+
    G: crypto::signature::Signer<crypto::Signature>,
+
{
    transport::local::register(storage.clone());

    let (working, head) = repository(path);
modified radicle/src/test/storage.rs
@@ -6,8 +6,9 @@ use std::str::FromStr;

use git_ext::ref_format as fmt;

-
use crate::crypto::{Signer, Verified};
+
use crate::crypto::Verified;
use crate::identity::doc::{Doc, DocAt, DocError, RawDoc, RepoId};
+
use crate::node::device::Device;
use crate::node::NodeId;

pub use crate::storage::*;
@@ -347,9 +348,9 @@ impl WriteRepository for MockRepository {
}

impl SignRepository for MockRepository {
-
    fn sign_refs<G: Signer>(
+
    fn sign_refs<G: crypto::signature::Signer<crypto::Signature>>(
        &self,
-
        _signer: &G,
+
        _signer: &Device<G>,
    ) -> Result<crate::storage::refs::SignedRefs<Verified>, RepositoryError> {
        todo!()
    }
@@ -363,6 +364,8 @@ impl radicle_cob::object::Storage for MockRepository {
    type UpdateError = Infallible;
    type RemoveError = Infallible;

+
    type Namespace = crypto::PublicKey;
+

    fn objects(
        &self,
        _typename: &radicle_cob::TypeName,
@@ -383,7 +386,7 @@ impl radicle_cob::object::Storage for MockRepository {

    fn update(
        &self,
-
        _identifier: &radicle_crypto::PublicKey,
+
        _namespace: &Self::Namespace,
        _typename: &radicle_cob::TypeName,
        _object_id: &radicle_cob::ObjectId,
        _entry: &radicle_cob::EntryId,
@@ -393,7 +396,7 @@ impl radicle_cob::object::Storage for MockRepository {

    fn remove(
        &self,
-
        _identifier: &radicle_crypto::PublicKey,
+
        _namespace: &Self::Namespace,
        _typename: &radicle_cob::TypeName,
        _object_id: &radicle_cob::ObjectId,
    ) -> Result<(), Self::RemoveError> {
@@ -419,7 +422,7 @@ impl radicle_cob::change::Storage for MockRepository {
        Self::StoreError,
    >
    where
-
        G: radicle_crypto::Signer,
+
        G: radicle_crypto::signature::Signer<Self::Signatures>,
    {
        todo!()
    }