Radish alpha
h
Radicle Heartwood Protocol & Stack
Radicle
Git (anonymous pull)
Log in to clone via SSH
Use `Signer::try_sign` in interactive contexts
Alexis Sellier committed 3 years ago
commit 577b6320562f361fe4c93029f79613a2bb02a3c5
parent a79ef67b2a6d391349064a6ba4b6fa6f8d875646
18 files changed +328 -207
modified radicle-crypto/src/lib.rs
@@ -45,6 +45,23 @@ pub trait Signer: Send + Sync {
    fn try_sign(&self, msg: &[u8]) -> Result<Signature, SignerError>;
}

+
impl<T> Signer for Box<T>
+
where
+
    T: Signer + ?Sized,
+
{
+
    fn public_key(&self) -> &PublicKey {
+
        self.deref().public_key()
+
    }
+

+
    fn sign(&self, msg: &[u8]) -> Signature {
+
        self.deref().sign(msg)
+
    }
+

+
    fn try_sign(&self, msg: &[u8]) -> Result<Signature, SignerError> {
+
        self.deref().try_sign(msg)
+
    }
+
}
+

/// Cryptographic signature.
#[derive(PartialEq, Eq, Copy, Clone)]
pub struct Signature(pub ed25519::Signature);
modified radicle-crypto/src/ssh/agent.rs
@@ -1,21 +1,92 @@
-
use radicle_ssh::agent::client::AgentClient;
-
use radicle_ssh::{self as ssh, agent::client::ClientStream};
+
use std::ops::{Deref, DerefMut};
+
use std::sync::Mutex;
+

+
pub use radicle_ssh::agent::client::AgentClient;
+
pub use radicle_ssh::agent::client::Error;
+
pub use radicle_ssh::{self as ssh, agent::client::ClientStream};

-
use crate as crypto;
use crate::ssh::SecretKey;
+
use crate::{self as crypto};
+
use crate::{PublicKey, Signature, Signer, SignerError};

#[cfg(not(unix))]
-
use std::net::TcpStream as Stream;
+
pub use std::net::TcpStream as Stream;
#[cfg(unix)]
-
use std::os::unix::net::UnixStream as Stream;
+
pub use std::os::unix::net::UnixStream as Stream;
+

+
pub struct Agent {
+
    client: AgentClient<Stream>,
+
}
+

+
impl Agent {
+
    /// Connect to a running SSH agent.
+
    pub fn connect() -> Result<Self, ssh::agent::client::Error> {
+
        Stream::connect_env().map(|client| Self { client })
+
    }
+

+
    /// Register a key with the agent.
+
    pub fn register(&mut self, key: &crypto::SecretKey) -> Result<(), ssh::Error> {
+
        self.client.add_identity(&SecretKey::from(*key), &[])
+
    }
+
}
+

+
impl Deref for Agent {
+
    type Target = AgentClient<Stream>;

-
pub fn connect() -> Result<AgentClient<Stream>, ssh::agent::client::Error> {
-
    Stream::connect_env()
+
    fn deref(&self) -> &Self::Target {
+
        &self.client
+
    }
}

-
pub fn register(key: &crypto::SecretKey) -> Result<(), ssh::agent::client::Error> {
-
    let mut agent = self::connect()?;
-
    agent.add_identity(&SecretKey::from(*key), &[])?;
+
impl DerefMut for Agent {
+
    fn deref_mut(&mut self) -> &mut Self::Target {
+
        &mut self.client
+
    }
+
}
+

+
/// A [`Signer`] that uses `ssh-agent`.
+
pub struct AgentSigner {
+
    agent: Mutex<Agent>,
+
    public: PublicKey,
+
}
+

+
impl AgentSigner {
+
    pub fn new(agent: Agent, public: PublicKey) -> Self {
+
        let agent = Mutex::new(agent);
+

+
        Self { agent, public }
+
    }
+

+
    pub fn is_ready(&self) -> Result<bool, Error> {
+
        let ids = self.agent.lock().unwrap().request_identities()?;
+

+
        Ok(ids.contains(&self.public))
+
    }
+

+
    /// Box this signer into a [`Signer`].
+
    pub fn boxed(self) -> Box<dyn Signer> {
+
        Box::new(self)
+
    }
+
}
+

+
impl Signer for AgentSigner {
+
    fn public_key(&self) -> &PublicKey {
+
        &self.public
+
    }
+

+
    fn sign(&self, msg: &[u8]) -> Signature {
+
        self.try_sign(msg).unwrap()
+
    }
+

+
    fn try_sign(&self, msg: &[u8]) -> Result<Signature, SignerError> {
+
        let sig = self
+
            .agent
+
            .lock()
+
            // We'll take our chances here; the worse that can happen is the agent returns an error.
+
            .unwrap_or_else(|e| e.into_inner())
+
            .sign(&self.public, msg)
+
            .map_err(SignerError::new)?;

-
    Ok(())
+
        Ok(Signature::from(sig))
+
    }
}
modified radicle-crypto/src/ssh/keystore.rs
@@ -1,11 +1,15 @@
+
use std::os::unix::fs::DirBuilderExt;
use std::path::{Path, PathBuf};
+
use std::{fs, io};

use thiserror::Error;

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

#[derive(Debug, Error)]
pub enum Error {
+
    #[error(transparent)]
+
    Io(#[from] io::Error),
    #[error("ssh keygen: {0}")]
    Ssh(#[from] ssh_key::Error),
    #[error("invalid key type, expected ed25519 key")]
@@ -28,17 +32,21 @@ impl Keystore {
        }
    }

-
    /// Initialize a keystore by generate a key pair and storing the secret and public keys
+
    /// Get the path to the keystore.
+
    pub fn path(&self) -> &Path {
+
        self.path.as_path()
+
    }
+

+
    /// Initialize a keystore by generating a key pair and storing the secret and public key
    /// at the given path.
    ///
-
    /// The comment is associated with the private key.
-
    /// The passphrase is used to encrypt the private key.
-
    ///
-
    pub fn init(self, comment: &str, passphrase: &str) -> Result<Self, Error> {
+
    /// The `comment` is associated with the private key.
+
    /// The `passphrase` is used to encrypt the private key.
+
    pub fn init(&self, comment: &str, passphrase: &str) -> Result<PublicKey, Error> {
        let pair = KeyPair::generate();
-
        let pair = ssh_key::private::Ed25519Keypair::from_bytes(&*pair)?;
-
        let pair = ssh_key::private::KeypairData::Ed25519(pair);
-
        let secret = ssh_key::PrivateKey::new(pair, comment)?;
+
        let ssh_pair = ssh_key::private::Ed25519Keypair::from_bytes(&*pair)?;
+
        let ssh_pair = ssh_key::private::KeypairData::Ed25519(ssh_pair);
+
        let secret = ssh_key::PrivateKey::new(ssh_pair, comment)?;
        let secret = secret.encrypt(ssh_key::rand_core::OsRng, passphrase)?;
        let public = secret.public_key();
        let path = self.path.join("radicle");
@@ -47,10 +55,15 @@ impl Keystore {
            return Err(Error::AlreadyInitialized);
        }

+
        fs::DirBuilder::new()
+
            .recursive(true)
+
            .mode(0o700)
+
            .create(&self.path)?;
+

        secret.write_openssh_file(&path, ssh_key::LineEnding::default())?;
        public.write_openssh_file(&path.with_extension("pub"))?;

-
        Ok(self)
+
        Ok(pair.pk.into())
    }

    /// Load the public key from the store. Returns `None` if it wasn't found.
@@ -61,10 +74,8 @@ impl Keystore {
        }

        let public = ssh_key::PublicKey::read_openssh_file(&path)?;
-
        match public.key_data() {
-
            ssh_key::public::KeyData::Ed25519(ssh_key::public::Ed25519PublicKey(data)) => {
-
                Ok(Some(PublicKey::from(*data)))
-
            }
+
        match public.try_into() {
+
            Ok(public) => Ok(Some(public)),
            _ => Err(Error::InvalidKeyType),
        }
    }
@@ -89,6 +100,70 @@ impl Keystore {
    }
}

+
#[derive(Debug, Error)]
+
pub enum MemorySignerError {
+
    #[error(transparent)]
+
    Keystore(#[from] Error),
+
    #[error("key not found in '{0}'")]
+
    NotFound(PathBuf),
+
}
+

+
/// An in-memory signer that keeps its secret key internally
+
/// so that signing never fails.
+
///
+
/// Can be created from a [`Keystore`] with the [`MemorySigner::load`] function.
+
#[derive(Debug)]
+
pub struct MemorySigner {
+
    public: PublicKey,
+
    secret: SecretKey,
+
}
+

+
impl Signer for MemorySigner {
+
    fn public_key(&self) -> &PublicKey {
+
        &self.public
+
    }
+

+
    fn sign(&self, msg: &[u8]) -> Signature {
+
        Signature(self.secret.sign(msg, None))
+
    }
+

+
    fn try_sign(&self, msg: &[u8]) -> Result<Signature, SignerError> {
+
        Ok(self.sign(msg))
+
    }
+
}
+

+
impl MemorySigner {
+
    /// Load this signer from a keystore, given a secret key passphrase.
+
    pub fn load(keystore: &Keystore, passphrase: &str) -> Result<Self, MemorySignerError> {
+
        let public = keystore
+
            .public_key()?
+
            .ok_or_else(|| MemorySignerError::NotFound(keystore.path().to_path_buf()))?;
+
        let secret = keystore
+
            .secret_key(passphrase)?
+
            .ok_or_else(|| MemorySignerError::NotFound(keystore.path().to_path_buf()))?;
+

+
        Ok(Self { public, secret })
+
    }
+

+
    /// Box this signer into a trait object.
+
    pub fn boxed(self) -> Box<dyn Signer> {
+
        Box::new(self)
+
    }
+
}
+

+
impl TryFrom<ssh_key::PublicKey> for PublicKey {
+
    type Error = Error;
+

+
    fn try_from(public: ssh_key::PublicKey) -> Result<Self, Self::Error> {
+
        match public.key_data() {
+
            ssh_key::public::KeyData::Ed25519(ssh_key::public::Ed25519PublicKey(data)) => {
+
                Ok(Self::from(*data))
+
            }
+
            _ => Err(Error::InvalidKeyType),
+
        }
+
    }
+
}
+

#[cfg(test)]
mod tests {
    use super::*;
@@ -98,11 +173,23 @@ mod tests {
        let tmp = tempfile::tempdir().unwrap();
        let store = Keystore::new(&tmp.path());

-
        let store = store.init("test", "hunter").unwrap();
-
        let public = store.public_key().unwrap().unwrap();
-
        let secret = store.secret_key("hunter").unwrap().unwrap();
+
        let public = store.init("test", "hunter").unwrap();
+
        assert_eq!(public, store.public_key().unwrap().unwrap());

+
        let secret = store.secret_key("hunter").unwrap().unwrap();
        assert_eq!(PublicKey::from(secret.public_key()), public);
+

        store.secret_key("blunder").unwrap_err(); // Wrong passphrase.
    }
+

+
    #[test]
+
    fn test_signer() {
+
        let tmp = tempfile::tempdir().unwrap();
+
        let store = Keystore::new(&tmp.path());
+

+
        let public = store.init("test", "hunter").unwrap();
+
        let signer = MemorySigner::load(&store, "hunter").unwrap();
+

+
        assert_eq!(public, *signer.public_key());
+
    }
}
modified radicle-node/src/client.rs
@@ -4,6 +4,8 @@ use crossbeam_channel as chan;
use nakamoto_net::{LocalTime, Reactor};
use thiserror::Error;

+
use radicle::crypto::Signer;
+

use crate::clock::RefClock;
use crate::profile::Profile;
use crate::service::routing;
@@ -70,7 +72,6 @@ impl Default for Config {

pub struct Client<R: Reactor> {
    reactor: R,
-
    profile: Profile,

    handle: chan::Sender<service::Command>,
    commands: chan::Receiver<service::Command>,
@@ -80,7 +81,7 @@ pub struct Client<R: Reactor> {
}

impl<R: Reactor> Client<R> {
-
    pub fn new(profile: Profile) -> Result<Self, Error> {
+
    pub fn new() -> Result<Self, Error> {
        let (handle, commands) = chan::unbounded::<service::Command>();
        let (shutdown, shutdown_recv) = chan::bounded(1);
        let (listening_send, listening) = chan::bounded(1);
@@ -88,7 +89,6 @@ impl<R: Reactor> Client<R> {
        let events = Events {};

        Ok(Self {
-
            profile,
            reactor,
            handle,
            commands,
@@ -98,13 +98,17 @@ impl<R: Reactor> Client<R> {
        })
    }

-
    pub fn run(mut self, config: Config) -> Result<(), Error> {
+
    pub fn run<G: Signer>(
+
        mut self,
+
        config: Config,
+
        profile: Profile,
+
        signer: G,
+
    ) -> Result<(), Error> {
        let network = config.service.network;
        let rng = fastrand::Rng::new();
        let time = LocalTime::now();
-
        let storage = self.profile.storage;
-
        let signer = self.profile.signer;
-
        let node_dir = self.profile.home.join(NODE_DIR);
+
        let storage = profile.storage;
+
        let node_dir = profile.home.join(NODE_DIR);
        let address_db = node_dir.join(ADDRESS_DB_FILE);
        let routing_db = node_dir.join(ROUTING_DB_FILE);

modified radicle-node/src/main.rs
@@ -1,8 +1,8 @@
-
use std::thread;
-
use std::{net, process};
+
use std::{env, net, process, thread};

use anyhow::Context as _;

+
use radicle_node::crypto::ssh::keystore::MemorySigner;
use radicle_node::logger;
use radicle_node::prelude::Address;
use radicle_node::{client, control, service};
@@ -48,8 +48,17 @@ fn main() -> anyhow::Result<()> {

    let options = Options::from_env()?;
    let profile = radicle::Profile::load().context("Failed to load node profile")?;
-
    let socket = profile.socket();
-
    let client = client::Client::<Reactor>::new(profile).context("Failed to initialize client")?;
+
    let node = profile.node();
+
    let client = client::Client::<Reactor>::new().context("Failed to initialize client")?;
+
    let signer = match profile.signer() {
+
        Ok(signer) => signer.boxed(),
+
        Err(err) => {
+
            let passphrase = env::var("RAD_PASSPHRASE")
+
                .context("Either ssh-agent must be initialized, or `RAD_PASSPHRASE` must be set")
+
                .context(err)?;
+
            MemorySigner::load(&profile.keystore, &passphrase)?.boxed()
+
        }
+
    };
    let handle = client.handle();
    let config = client::Config {
        service: service::Config {
@@ -59,8 +68,8 @@ fn main() -> anyhow::Result<()> {
        listen: options.listen,
    };

-
    let t1 = thread::spawn(move || control::listen(socket, handle));
-
    let t2 = thread::spawn(move || client.run(config));
+
    let t1 = thread::spawn(move || control::listen(node, handle));
+
    let t2 = thread::spawn(move || client.run(config, profile, signer));

    t1.join().unwrap()?;
    t2.join().unwrap()?;
modified radicle-remote-helper/src/lib.rs
@@ -4,8 +4,7 @@ use std::{env, io, process};

use thiserror::Error;

-
use radicle::crypto::ssh;
-
use radicle::crypto::{PublicKey, Signer};
+
use radicle::crypto::PublicKey;
use radicle::node::Handle;
use radicle::storage::git::transport::local::{Url, UrlError};
use radicle::storage::{ReadRepository, WriteRepository, WriteStorage};
@@ -52,9 +51,7 @@ pub fn run(profile: radicle::Profile) -> Result<(), Box<dyn std::error::Error +
        }
    }?;
    // Default to profile key.
-
    let namespace = url
-
        .namespace
-
        .unwrap_or_else(|| *profile.signer.public_key());
+
    let namespace = url.namespace.unwrap_or(profile.public_key);

    let proj = profile.storage.repository(url.repo)?;
    if proj.is_empty()? {
@@ -83,17 +80,16 @@ pub fn run(profile: radicle::Profile) -> Result<(), Box<dyn std::error::Error +
                // 1. Our key is not in ssh-agent, which means we won't be able to sign the refs.
                // 2. Our key is not the one loaded in the profile, which means that the signed refs
                //    won't match the remote we're pushing to.
-
                if *service == GIT_RECEIVE_PACK {
-
                    if profile.signer.public_key() != &namespace {
+
                let signer = if *service == GIT_RECEIVE_PACK {
+
                    if profile.public_key != namespace {
                        return Err(Error::KeyMismatch(namespace).into());
                    }
-
                    if !ssh::agent::connect()?
-
                        .request_identities::<PublicKey>()?
-
                        .contains(&namespace)
-
                    {
-
                        return Err(Error::KeyNotRegistered(namespace).into());
-
                    }
-
                }
+
                    let signer = profile.signer()?;
+

+
                    Some(signer)
+
                } else {
+
                    None
+
                };
                println!(); // Empty line signifies connection is established.

                let mut child = process::Command::new(service)
@@ -106,13 +102,13 @@ pub fn run(profile: radicle::Profile) -> Result<(), Box<dyn std::error::Error +
                    .spawn()?;

                if child.wait()?.success() {
-
                    if *service == GIT_RECEIVE_PACK {
-
                        proj.sign_refs(&profile.signer)?;
+
                    if let Some(signer) = signer {
+
                        proj.sign_refs(&signer)?;
                        proj.set_head()?;
                        // Connect to local node and announce refs to the network.
                        // If our node is not running, we simply skip this step, as the
                        // refs will be announced eventually, when the node restarts.
-
                        if let Ok(conn) = profile.node() {
+
                        if let Ok(conn) = radicle::node::connect(&profile.node()) {
                            conn.announce_refs(&url.repo)?;
                        }
                    }
modified radicle-ssh/src/agent/client.rs
@@ -54,7 +54,7 @@ impl<S> AgentClient<S> {
}

pub trait ClientStream: Sized + Send + Sync {
-
    /// Send an agent request to through the stream and read the response.
+
    /// Send an agent request through the stream and read the response.
    fn request(&mut self, req: &[u8]) -> Result<Buffer, Error>;

    /// How to connect the streaming socket
modified radicle-ssh/src/lib.rs
@@ -1,2 +1,4 @@
pub mod agent;
pub mod encoding;
+

+
pub use agent::client::Error;
modified radicle-tools/src/rad-agent.rs
@@ -1,30 +1,39 @@
+
use anyhow::anyhow;
use radicle::{crypto, crypto::ssh};
use std::io::prelude::*;
use std::{env, io};

fn main() -> anyhow::Result<()> {
    let profile = radicle::Profile::load()?;
+
    let mut agent = ssh::agent::Agent::connect()?;

    println!("({})", ssh::fmt::key(profile.id()));

    match env::args().nth(1).as_deref() {
        Some("add") => {
-
            ssh::agent::register(&profile.signer.secret)?;
+
            let mut passphrase = String::new();
+
            io::stdin().lock().read_line(&mut passphrase)?;
+

+
            let secret = profile
+
                .keystore
+
                .secret_key(&passphrase)?
+
                .ok_or_else(|| anyhow!("Key not found in {:?}", profile.keystore.path()))?;
+

+
            agent.register(&secret)?;
            println!("ok");
        }
        Some("remove") => {
-
            ssh::agent::connect()?.remove_identity(profile.id())?;
+
            agent.remove_identity(profile.id())?;
            println!("ok");
        }
        Some("remove-all") => {
-
            ssh::agent::connect()?.remove_all_identities()?;
+
            agent.remove_all_identities()?;
            println!("ok");
        }
        Some("sign") => {
            let mut stdin = Vec::new();
            io::stdin().read_to_end(&mut stdin)?;

-
            let mut agent = ssh::agent::connect()?;
            let sig = agent.sign(profile.id(), &stdin)?;
            let sig = crypto::Signature::from(sig);

modified radicle-tools/src/rad-auth.rs
@@ -1,16 +1,12 @@
-
use radicle::profile;
-
use radicle::Profile;
+
use radicle::profile::{Error, Profile};

fn main() -> anyhow::Result<()> {
    let profile = match Profile::load() {
-
        Ok(v) => v,
-
        Err(profile::Error::NotFound(_)) => {
-
            let keypair = radicle::crypto::KeyPair::generate();
-
            radicle::crypto::ssh::agent::register(&keypair.sk)?;
-
            radicle::Profile::init(keypair)?
-
        }
+
        Ok(profile) => profile,
+
        Err(Error::NotFound(_)) => Profile::init("radicle")?,
        Err(err) => anyhow::bail!(err),
    };
+

    println!("id: {}", profile.id());
    println!("home: {}", profile.home.display());

modified radicle-tools/src/rad-clone.rs
@@ -7,11 +7,12 @@ use radicle::identity::Id;
fn main() -> anyhow::Result<()> {
    let cwd = Path::new(".").canonicalize()?;
    let profile = radicle::Profile::load()?;
+
    let signer = profile.signer()?;

    if let Some(id) = env::args().nth(1) {
        let id = Id::from_str(&id)?;
-
        let node = profile.node()?;
-
        let repo = radicle::rad::clone(id, &cwd, &profile.signer, &profile.storage, &node)?;
+
        let node = radicle::node::connect(profile.node())?;
+
        let repo = radicle::rad::clone(id, &cwd, &signer, &profile.storage, &node)?;

        println!(
            "ok: project {id} cloned into `{}`",
modified radicle-tools/src/rad-init.rs
@@ -1,18 +1,19 @@
use std::path::Path;

-
use radicle::git;
+
use radicle::{git, Profile};

fn main() -> anyhow::Result<()> {
    let cwd = Path::new(".").canonicalize()?;
    let name = cwd.file_name().unwrap().to_string_lossy().to_string();
    let repo = radicle::git::raw::Repository::open(cwd)?;
-
    let profile = radicle::Profile::load()?;
+
    let profile = Profile::load()?;
+
    let signer = profile.signer()?;
    let (id, _) = radicle::rad::init(
        &repo,
        &name,
        "",
        git::refname!("master"),
-
        &profile.signer,
+
        &signer,
        &profile.storage,
    )?;

modified radicle-tools/src/rad-push.rs
@@ -11,11 +11,12 @@ fn main() -> anyhow::Result<()> {
    let output = radicle::git::run::<_, _, &str, &str>(&cwd, &["push", "rad"], None)?;
    println!("{}", output);

+
    let signer = profile.signer()?;
    let project = profile.storage.repository(id)?;
-
    let sigrefs = project.sign_refs(&profile.signer)?;
+
    let sigrefs = project.sign_refs(&signer)?;
    let head = project.set_head()?;

-
    profile.node()?.announce_refs(&id)?;
+
    radicle::node::connect(&profile.node())?.announce_refs(&id)?;

    println!("head: {}", head);
    println!("ok: {}", sigrefs.signature);
modified radicle/src/identity/project.rs
@@ -486,7 +486,7 @@ impl Identity<Untrusted> {
#[cfg(test)]
mod test {
    use radicle_crypto::test::signer::MockSigner;
-
    use radicle_crypto::Signer;
+
    use radicle_crypto::Signer as _;

    use crate::rad;
    use crate::storage::git::Storage;
deleted radicle/src/keystore.rs
@@ -1,78 +0,0 @@
-
use std::fs;
-
use std::io;
-
use std::io::Write;
-
use std::os::unix::fs::DirBuilderExt;
-
use std::os::unix::prelude::OpenOptionsExt;
-
use std::path::{Path, PathBuf};
-

-
use thiserror::Error;
-

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

-
#[derive(Debug, Error)]
-
pub enum Error {
-
    #[error(transparent)]
-
    Io(#[from] io::Error),
-
    #[error("invalid key file format: {0}")]
-
    FromUtf8Error(#[from] std::string::FromUtf8Error),
-
    #[error("invalid key format: {0}")]
-
    Crypto(#[from] crypto::Error),
-
}
-

-
pub struct UnsafeKeystore {
-
    path: PathBuf,
-
}
-

-
impl UnsafeKeystore {
-
    pub fn new<P: AsRef<Path>>(path: &P) -> Self {
-
        Self {
-
            path: path.as_ref().to_path_buf(),
-
        }
-
    }
-

-
    pub fn put(&mut self, public: &PublicKey, secret: &SecretKey) -> Result<(), Error> {
-
        // TODO: Zeroize secret key.
-
        let public = public.to_pem();
-
        let secret = secret.to_pem();
-

-
        fs::DirBuilder::new()
-
            .recursive(true)
-
            .mode(0o700)
-
            .create(&self.path)?;
-

-
        fs::OpenOptions::new()
-
            .mode(0o644)
-
            .create_new(true)
-
            .write(true)
-
            .open(self.path.join("radicle.pub"))?
-
            .write_all(public.as_bytes())?;
-

-
        fs::OpenOptions::new()
-
            .mode(0o600)
-
            .create_new(true)
-
            .write(true)
-
            .open(self.path.join("radicle"))?
-
            .write_all(secret.as_bytes())?;
-

-
        Ok(())
-
    }
-

-
    pub fn get(&self) -> Result<Option<(PublicKey, SecretKey)>, Error> {
-
        let public = self.path.join("radicle.pub");
-
        let secret = self.path.join("radicle");
-
        if !public.exists() && !secret.exists() {
-
            return Ok(None);
-
        }
-

-
        let public = fs::read(public)?;
-
        let public = String::from_utf8(public)?;
-
        let public = PublicKey::from_pem(&public)?;
-

-
        let secret = fs::read(secret)?;
-
        let secret = String::from_utf8(secret)?;
-
        let secret = SecretKey::from_pem(&secret)?;
-

-
        Ok(Some((public, secret)))
-
    }
-
}
modified radicle/src/lib.rs
@@ -7,7 +7,6 @@ pub mod collections;
pub mod git;
pub mod hash;
pub mod identity;
-
pub mod keystore;
pub mod node;
pub mod profile;
pub mod rad;
@@ -18,6 +17,5 @@ pub mod storage;
#[cfg(any(test, feature = "test"))]
pub mod test;

-
pub use keystore::UnsafeKeystore;
pub use profile::Profile;
pub use storage::git::Storage;
modified radicle/src/node.rs
@@ -33,19 +33,21 @@ pub trait Handle {
    fn shutdown(self) -> Result<(), Error>;
}

-
/// Node control socket.
+
/// Node controller.
#[derive(Debug)]
-
pub struct Connection {
+
pub struct Node {
    stream: UnixStream,
}

-
impl Connection {
+
impl Node {
+
    /// Connect to the node, via the socket at the given path.
    pub fn connect<P: AsRef<Path>>(path: P) -> Result<Self, Error> {
        let stream = UnixStream::connect(path).map_err(Error::Connect)?;

        Ok(Self { stream })
    }

+
    /// Call a command on the node.
    pub fn call<A: fmt::Display>(
        &self,
        cmd: &str,
@@ -57,7 +59,7 @@ impl Connection {
    }
}

-
impl Handle for Connection {
+
impl Handle for Node {
    fn fetch(&self, id: &Id) -> Result<(), Error> {
        for line in self.call("fetch", id)? {
            let line = line?;
@@ -94,3 +96,8 @@ impl Handle for Connection {
        todo!();
    }
}
+

+
/// Connect to the local node.
+
pub fn connect<P: AsRef<Path>>(path: P) -> Result<Node, Error> {
+
    Node::connect(path)
+
}
modified radicle/src/profile.rs
@@ -15,8 +15,9 @@ use std::{env, io};

use thiserror::Error;

-
use crate::crypto::{KeyPair, PublicKey, SecretKey, Signature, Signer, SignerError};
-
use crate::keystore::UnsafeKeystore;
+
use crate::crypto::ssh::agent::{Agent, AgentSigner};
+
use crate::crypto::ssh::keystore::Keystore;
+
use crate::crypto::PublicKey;
use crate::node;
use crate::storage::git::transport;
use crate::storage::git::Storage;
@@ -26,87 +27,86 @@ pub enum Error {
    #[error(transparent)]
    Io(#[from] io::Error),
    #[error(transparent)]
-
    KeyStore(#[from] crate::keystore::Error),
+
    Keystore(#[from] crate::crypto::ssh::keystore::Error),
    #[error("no profile found at the filepath '{0}'")]
    NotFound(PathBuf),
-
}
-

-
#[derive(Debug)]
-
pub struct UnsafeSigner {
-
    pub public: PublicKey,
-
    pub secret: SecretKey,
-
}
-

-
impl Signer for UnsafeSigner {
-
    fn public_key(&self) -> &PublicKey {
-
        &self.public
-
    }
-

-
    fn sign(&self, msg: &[u8]) -> Signature {
-
        Signature(self.secret.sign(msg, None))
-
    }
-

-
    fn try_sign(&self, msg: &[u8]) -> Result<Signature, SignerError> {
-
        Ok(self.sign(msg))
-
    }
+
    #[error("error connecting to ssh-agent: {0}")]
+
    Agent(#[from] crate::crypto::ssh::agent::Error),
+
    #[error("profile key `{0}` is not registered with ssh-agent")]
+
    KeyNotRegistered(PublicKey),
}

#[derive(Debug)]
pub struct Profile {
    pub home: PathBuf,
-
    pub signer: UnsafeSigner,
    pub storage: Storage,
+
    pub keystore: Keystore,
+
    pub public_key: PublicKey,
}

impl Profile {
-
    pub fn init(keypair: KeyPair) -> Result<Self, Error> {
+
    pub fn init(passphrase: &str) -> Result<Self, Error> {
        let home = self::home()?;
-
        let mut keystore = UnsafeKeystore::new(&home.join("keys"));
-
        let public = keypair.pk.into();
-
        let signer = UnsafeSigner {
-
            public,
-
            secret: keypair.sk,
-
        };
        let storage = Storage::open(&home.join("storage"))?;
+
        // TODO: This should generate a keypair at the given location.
+
        // For now, we use ssh-keygen?
+
        let keystore = Keystore::new(&home.join("keys"));
+
        let public_key = keystore.init("radicle", passphrase)?;

        transport::local::register(storage.clone());
-
        keystore.put(&signer.public, &signer.secret)?;

        Ok(Profile {
            home,
-
            signer,
            storage,
+
            keystore,
+
            public_key,
        })
    }

    pub fn load() -> Result<Self, Error> {
        let home = self::home()?;
-
        let (public, secret) = UnsafeKeystore::new(&home.join("keys"))
-
            .get()?
-
            .ok_or_else(|| Error::NotFound(home.clone()))?;
-
        let signer = UnsafeSigner { public, secret };
        let storage = Storage::open(&home.join("storage"))?;
+
        let keystore = Keystore::new(&home.join("keys"));
+
        let public_key = keystore
+
            .public_key()?
+
            .ok_or_else(|| Error::NotFound(home.clone()))?;

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

        Ok(Profile {
            home,
-
            signer,
            storage,
+
            keystore,
+
            public_key,
        })
    }

-
    /// Return a connection to the locally running node.
-
    pub fn node(&self) -> Result<node::Connection, node::Error> {
-
        node::Connection::connect(self.socket())
+
    pub fn id(&self) -> &PublicKey {
+
        &self.public_key
    }

-
    pub fn id(&self) -> &PublicKey {
-
        self.signer.public_key()
+
    pub fn signer(&self) -> Result<AgentSigner, Error> {
+
        match Agent::connect() {
+
            Ok(agent) => {
+
                let signer = AgentSigner::new(agent, self.public_key);
+

+
                if signer.is_ready()? {
+
                    Ok(signer)
+
                } else {
+
                    Err(Error::KeyNotRegistered(self.public_key))
+
                }
+
            }
+
            Err(err) => Err(err.into()),
+
        }
+
    }
+

+
    /// Return the path to the keys folder.
+
    pub fn keys(&self) -> PathBuf {
+
        self.home.join("keys")
    }

    /// Get the path to the radicle node socket.
-
    pub fn socket(&self) -> PathBuf {
+
    pub fn node(&self) -> PathBuf {
        env::var_os("RAD_SOCKET")
            .map(PathBuf::from)
            .unwrap_or_else(|| self.home.join("node").join(node::DEFAULT_SOCKET_NAME))