Use `Signer::try_sign` in interactive contexts
In interactive (user) contexts, use `try_sign` instead of `sign`. We also revisit the `Profile` by adding a `signer` method that can fail, eg. if we couldn't connect to ssh-agent. Finally, we replace `UnsafeSigner` with `MemorySigner`, which makes use of `ssh::Keystore`. Signed-off-by: Alexis Sellier <alexis@radicle.xyz>
This commit is contained in:
parent
a79ef67b2a
commit
577b632056
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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 fn connect() -> Result<AgentClient<Stream>, ssh::agent::client::Error> {
|
||||
Stream::connect_env()
|
||||
pub struct Agent {
|
||||
client: AgentClient<Stream>,
|
||||
}
|
||||
|
||||
pub fn register(key: &crypto::SecretKey) -> Result<(), ssh::agent::client::Error> {
|
||||
let mut agent = self::connect()?;
|
||||
agent.add_identity(&SecretKey::from(*key), &[])?;
|
||||
|
||||
Ok(())
|
||||
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>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.client
|
||||
}
|
||||
}
|
||||
|
||||
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(Signature::from(sig))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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()?;
|
||||
|
|
|
|||
|
|
@ -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)?;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -1,2 +1,4 @@
|
|||
pub mod agent;
|
||||
pub mod encoding;
|
||||
|
||||
pub use agent::client::Error;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
|
||||
|
|
|
|||
|
|
@ -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 `{}`",
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)?;
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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)))
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
Loading…
Reference in New Issue