diff --git a/Cargo.toml b/Cargo.toml index 8d1a230a4..527638ee4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,3 +27,6 @@ rev = "ae027b9e7b125f56397bbb7d8652b3427deeede6" git = "https://github.com/radicle-dev/git2-rs.git" rev = "ae027b9e7b125f56397bbb7d8652b3427deeede6" +[patch.crates-io.thrussh-encoding] +git = "https://github.com/FintanH/thrussh.git" +branch = "generic-agent" diff --git a/clib/Cargo.toml b/clib/Cargo.toml index 91f006c89..9c929aff9 100644 --- a/clib/Cargo.toml +++ b/clib/Cargo.toml @@ -23,3 +23,8 @@ path = "../librad" [dependencies.minicbor] version = "0.9.1" features = ["std"] + +[dependencies.thrussh-agent] +git = "https://github.com/FintanH/thrussh" +branch = "generic-agent" +default-features = false diff --git a/clib/src/keys.rs b/clib/src/keys.rs index 3fda13478..6b59aa3df 100644 --- a/clib/src/keys.rs +++ b/clib/src/keys.rs @@ -3,18 +3,26 @@ // This file is part of radicle-link, distributed under the GPLv3 with Radicle // Linking Exception. For full terms see the included LICENSE file. +use std::sync::Arc; + +use thiserror::Error; +use thrussh_agent::client::ClientStream; + use librad::{ crypto::{ keystore::{ crypto::{Crypto, KdfParams, Pwhash, SecretBoxError}, file, pinentry::Prompt, + sign::ssh::{self, SshAgent}, FileStorage, Keystore as _, }, BoxedSigner, IntoSecretKeyError, + SomeSigner, }, + git::storage::ReadOnly, profile::Profile, PublicKey, SecretKey, @@ -23,7 +31,13 @@ use librad::{ /// The filename for storing the secret key. pub const LIBRAD_KEY_FILE: &str = "librad.key"; -pub type Error = file::Error, IntoSecretKeyError>; +#[derive(Debug, Error)] +pub enum Error { + #[error(transparent)] + File(#[from] file::Error, IntoSecretKeyError>), + #[error(transparent)] + SshConnect(#[from] ssh::error::Connect), +} /// Create a [`Prompt`] for unlocking the key storage. pub fn prompt() -> Pwhash> { @@ -61,6 +75,20 @@ pub fn signer_prompt(profile: &Profile) -> Result { Ok(key.into()) } +pub async fn signer_ssh(profile: &Profile) -> Result +where + S: ClientStream + Unpin + 'static, +{ + let storage = ReadOnly::open(profile.paths()).unwrap(); + let peer_id = storage.peer_id(); + let agent = SshAgent::new((**peer_id).into()); + let signer = agent.connect::().await?; + Ok(SomeSigner { + signer: Arc::new(signer), + } + .into()) +} + /// Get the signer from the file store, decrypting the secret key by asking for /// a passphrase via a prompt. /// diff --git a/clib/src/storage.rs b/clib/src/storage.rs index 1a19065be..2db1f7855 100644 --- a/clib/src/storage.rs +++ b/clib/src/storage.rs @@ -6,6 +6,7 @@ use thiserror::Error; use librad::{ + crypto::BoxedSigner, git::storage::{error, read, ReadOnly, Storage}, profile::Profile, }; @@ -22,28 +23,41 @@ pub enum Error { Keys(#[from] super::keys::Error), } -/// How to decrypt the secret key from the file store when initialising the -/// [`Storage`]. -pub enum Crypto { - /// The decryption will happen by prompting the person for their passphrase - /// at the command line. - Prompt, - // TODO(finto): SshAgent -} - /// Intialise a [`ReadOnly`] storage. pub fn read_only(profile: &Profile) -> Result { let paths = profile.paths(); Ok(ReadOnly::open(paths)?) } -/// Initialise [`Storage`] based on the [`Crypto`] provided. -pub fn read_write(profile: &Profile, crypto: Crypto) -> Result { - let paths = profile.paths(); - match crypto { - Crypto::Prompt => { - let signer = keys::signer_prompt(profile)?; - Ok(Storage::open(paths, signer)?) - }, +pub mod prompt { + use super::*; + + /// Initialise [`Storage`]. + /// + /// The decryption will happen by prompting the person for their passphrase + /// at the command line. + pub fn storage(profile: &Profile) -> Result<(BoxedSigner, Storage), Error> { + let paths = profile.paths(); + let signer = keys::signer_prompt(profile)?; + Ok((signer.clone(), Storage::open(paths, signer)?)) + } +} + +pub mod ssh { + use thrussh_agent::client::ClientStream; + + use super::*; + + /// Initialise [`Storage`]. + /// + /// The signing key will be retrieved from the ssh-agent. If the key was not + /// added to the agent then this result in an error. + pub async fn storage(profile: &Profile) -> Result<(BoxedSigner, Storage), Error> + where + S: ClientStream + Unpin + 'static, + { + let paths = profile.paths(); + let signer = keys::signer_ssh::(profile).await?; + Ok((signer.clone(), Storage::open(paths, signer)?)) } } diff --git a/deny.toml b/deny.toml index 923da8be2..fe7bed93d 100644 --- a/deny.toml +++ b/deny.toml @@ -203,6 +203,8 @@ unknown-git = "deny" allow-registry = ["https://github.com/rust-lang/crates.io-index"] # List of URLs for allowed Git repositories allow-git = [ + "https://github.com/FintanH/thrussh", "https://github.com/ZcashFoundation/ed25519-zebra", "https://github.com/radicle-dev/git2-rs", + "https://github.com/radicle-dev/radicle-keystore", ] diff --git a/link-crypto/Cargo.toml b/link-crypto/Cargo.toml index 947e28192..d7ddd2d38 100644 --- a/link-crypto/Cargo.toml +++ b/link-crypto/Cargo.toml @@ -14,7 +14,6 @@ async-trait = "0.1" dyn-clone = "1.0" futures-lite = "1.12.0" multibase = "0.9" -radicle-keystore = "0.1.1" rand = "0.7" rustls = "0.19" thiserror = "1.0" @@ -33,6 +32,11 @@ features = ["std", "derive"] path = "../git-ext" features = ["serde", "minicbor"] +[dependencies.radicle-keystore] +git = "https://github.com/radicle-dev/radicle-keystore" +rev = "619ca3600be58025f1f2b2fcc59d5ba72f52141f" +features = [ "ssh-agent" ] + [dependencies.serde] version = "1.0" features = ["derive"] diff --git a/link-crypto/src/keys.rs b/link-crypto/src/keys.rs index adba9580f..c7da327b6 100644 --- a/link-crypto/src/keys.rs +++ b/link-crypto/src/keys.rs @@ -48,6 +48,12 @@ impl From for PublicKey { } } +impl From for sign::PublicKey { + fn from(other: PublicKey) -> Self { + Self(other.0.into()) + } +} + /// A signature produced by `Key::sign` #[derive(Clone, Debug, Eq, PartialEq)] pub struct Signature(ed25519::Signature);