Radish alpha
h
Radicle Heartwood Protocol & Stack
Radicle
Git (anonymous pull)
Log in to clone via SSH
radicle: move to `signature` crate
Fintan Halpenny committed 11 months ago
commit 1f4fcc5e6a3dadef99284bc3845ada11c74ccd97
parent 41f9048d96596473600e6c77f041d29cd3de1d6f
60 files changed +930 -429
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,11 @@ 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::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 +723,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,
+
    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/issue.rs
@@ -10,8 +10,9 @@ 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::prelude::{Did, RepoId};
use radicle::profile;
use radicle::storage;
@@ -810,12 +811,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,
+
    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 +846,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,
+
    G: crypto::signature::Signer<crypto::Signature>,
{
    let id = id.resolve(&repo.backend)?;
    let mut issue = issues.get_mut(&id)?;
modified radicle-cli/src/commands/job.rs
@@ -4,7 +4,8 @@ use std::ffi::OsString;
use anyhow::{anyhow, Context as _};

use radicle::cob::job::{JobStore, Reason, State};
-
use radicle::crypto::Signer;
+
use radicle::crypto;
+
use radicle::node::device::Device;
use radicle::node::Handle;
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,
+
    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,
+
    G: crypto::signature::Signer<crypto::Signature>,
+
{
    let job_id = job_id.resolve(&repo.backend)?;
    let mut job = store.get_mut(&job_id)?;

@@ -349,13 +358,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,
+
    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/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]
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
@@ -64,7 +64,7 @@ pub fn create<T, S, G>(
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/update.rs
@@ -7,7 +7,7 @@ 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;
@@ -67,7 +67,7 @@ pub fn update<T, S, G>(
where
    T: Evaluate<S>,
    S: Store,
-
    G: crypto::Signer,
+
    G: signature::Signer<ExtendedSignature>,
{
    let Update {
        type_name: ref typename,
modified radicle-cob/src/test/storage.rs
@@ -6,7 +6,7 @@ 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)
    }
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-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,13 @@ 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::{
    cob,
    cob::{
@@ -192,11 +192,14 @@ impl Identity {
        }
    }

-
    pub fn initialize<'a, R: WriteRepository + cob::Store, G: Signer>(
+
    pub fn initialize<'a, R: WriteRepository + cob::Store, 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>,
+
    {
        let mut store = cob::store::Store::open(store)?;
        let (id, identity) = Transaction::<Identity, _>::initial(
            "Initialize identity",
@@ -732,7 +735,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 +845,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.
@@ -901,11 +907,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 +925,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 +944,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 +956,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 +1030,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 +1059,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 +1119,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 +1542,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,8 @@ 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::prelude::{Did, Doc, ReadRepository, RepoId};
use crate::storage::{HasRepoId, RepositoryError, WriteRepository};

@@ -608,26 +608,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 +645,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();
@@ -791,10 +816,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 +847,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 +1743,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,7 @@ 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::prelude::{Did, RepoId};
use crate::storage::{HasRepoId, ReadRepository, RepositoryError, SignRepository, WriteRepository};

@@ -104,11 +104,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,
+
        G: crypto::signature::Signer<crypto::Signature>,
        C: Update<Issue>,
    {
        self.store.create(
@@ -124,9 +124,9 @@ 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,
+
        G: crypto::signature::Signer<crypto::Signature>,
        R: ReadRepository + SignRepository + cob::Store,
        C: Remove<Issue>,
    {
modified radicle/src/cob/job.rs
@@ -19,8 +19,8 @@ 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::prelude::ReadRepository;
use crate::storage::{Oid, WriteRepository};

@@ -332,12 +332,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 +348,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();
@@ -421,11 +427,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 +448,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);
@@ -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))
    }
}
@@ -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,8 +9,8 @@ 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};

@@ -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,
+
        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,
+
        G: crypto::signature::Signer<crypto::Signature>,
        C: Update<Patch>,
    {
        self.store.draft(
@@ -167,9 +167,9 @@ 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,
+
        G: crypto::signature::Signer<crypto::Signature>,
        R: ReadRepository + SignRepository + cob::Store,
        C: Remove<Patch>,
    {
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;
@@ -208,15 +209,18 @@ where
    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,13 +415,13 @@ 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,
        T::Action: Serialize + Clone,
@@ -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,
        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))
                }
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;
@@ -11,6 +12,9 @@ use storage::SignRepository;
use storage::ValidateRepository;

use crate::git::*;
+
use crate::identity;
+
use crate::identity::doc::DocError;
+
use crate::node::device::Device;
use crate::storage;
use crate::storage::Error;
use crate::storage::{
@@ -68,7 +72,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)
    }
@@ -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.
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!()
    }
@@ -419,7 +420,7 @@ impl radicle_cob::change::Storage for MockRepository {
        Self::StoreError,
    >
    where
-
        G: radicle_crypto::Signer,
+
        G: radicle_crypto::signature::Signer<Self::Signatures>,
    {
        todo!()
    }