diff --git a/crates/radicle-cli/src/commands/self.rs b/crates/radicle-cli/src/commands/self.rs index aad6768a..79de897c 100644 --- a/crates/radicle-cli/src/commands/self.rs +++ b/crates/radicle-cli/src/commands/self.rs @@ -146,7 +146,9 @@ fn all(profile: &Profile) -> anyhow::Result<()> { let ssh_agent = match ssh::agent::Agent::connect() { Ok(c) => term::format::positive(format!( "running ({})", - c.pid().map(|p| p.to_string()).unwrap_or(String::from("?")) + c.path() + .map(|p| p.display().to_string()) + .unwrap_or(String::from("?")) )), Err(e) if e.is_not_running() => term::format::yellow(String::from("not running")), Err(e) => term::format::negative(format!("error: {e}")), diff --git a/crates/radicle-crypto/src/ssh.rs b/crates/radicle-crypto/src/ssh.rs index 474ac92b..d1d045b5 100644 --- a/crates/radicle-crypto/src/ssh.rs +++ b/crates/radicle-crypto/src/ssh.rs @@ -261,13 +261,6 @@ mod test { } impl ClientStream for DummyStream { - fn connect

(_path: P) -> Result, Error> - where - P: AsRef + Send, - { - panic!("This function should never be called!") - } - fn request(&mut self, buf: &[u8]) -> Result { *self.incoming.lock().unwrap() = buf.to_vec(); @@ -304,7 +297,7 @@ mod test { ]; let stream = DummyStream::default(); - let mut agent = AgentClient::connect(stream.clone()); + let mut agent = AgentClient::new(None, stream.clone()); agent.remove_identity(&pk).unwrap(); @@ -334,7 +327,7 @@ mod test { ]; let stream = DummyStream::default(); - let mut agent = AgentClient::connect(stream.clone()); + let mut agent = AgentClient::new(None, stream.clone()); let data: Vec = vec![1, 2, 3, 4, 5, 6, 7, 8, 9]; agent.sign(&pk, &data).ok(); diff --git a/crates/radicle-crypto/src/ssh/agent.rs b/crates/radicle-crypto/src/ssh/agent.rs index 80419140..05a6f956 100644 --- a/crates/radicle-crypto/src/ssh/agent.rs +++ b/crates/radicle-crypto/src/ssh/agent.rs @@ -1,26 +1,23 @@ use std::cell::RefCell; +use std::path::Path; -pub use radicle_ssh::agent::client::AgentClient; -pub use radicle_ssh::agent::client::Error; -pub use radicle_ssh::{self as ssh, agent::client::ClientStream}; +pub use radicle_ssh as ssh; +pub use ssh::agent::client::{AgentClient, Error}; use crate::{PublicKey, SecretKey, Signature, Signer, SignerError}; -#[cfg(not(unix))] -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, + client: AgentClient, } impl Agent { /// Connect to a running SSH agent. - pub fn connect() -> Result { - Stream::connect_env().map(|client| Self { client }) + pub fn connect() -> Result { + Ok(Self { + client: AgentClient::connect_env()?, + }) } /// Register a key with the agent. @@ -45,8 +42,8 @@ impl Agent { AgentSigner::new(self, key) } - pub fn pid(&self) -> Option { - self.client.pid() + pub fn path(&self) -> Option<&Path> { + self.client.path() } pub fn request_identities(&mut self) -> Result, ssh::agent::client::Error> { diff --git a/crates/radicle-ssh/Cargo.toml b/crates/radicle-ssh/Cargo.toml index a5a9d60f..9b6d147b 100644 --- a/crates/radicle-ssh/Cargo.toml +++ b/crates/radicle-ssh/Cargo.toml @@ -18,3 +18,6 @@ byteorder = "1.4" log = { workspace = true } thiserror = { workspace = true } zeroize = { workspace = true } + +[target.'cfg(windows)'.dependencies] +winpipe = { workspace = true } \ No newline at end of file diff --git a/crates/radicle-ssh/src/agent/client.rs b/crates/radicle-ssh/src/agent/client.rs index 881c16fc..462f889f 100644 --- a/crates/radicle-ssh/src/agent/client.rs +++ b/crates/radicle-ssh/src/agent/client.rs @@ -1,11 +1,15 @@ use std::fmt; use std::io::{Read, Write}; use std::ops::DerefMut; -use std::os::unix::net::UnixStream; -use std::path::Path; -use std::str::FromStr; +use std::path::{Path, PathBuf}; -use byteorder::{BigEndian, ByteOrder, WriteBytesExt}; +#[cfg(unix)] +pub use std::os::unix::net::UnixStream as Stream; + +#[cfg(windows)] +pub use winpipe::WinStream as Stream; + +use byteorder::{BigEndian, ByteOrder as _, WriteBytesExt}; use log::*; use thiserror::Error; use zeroize::Zeroize as _; @@ -21,74 +25,103 @@ pub type Signature = [u8; 64]; #[derive(Debug, Error)] pub enum Error { /// Agent protocol error. - #[error("Agent protocol error")] + #[error("SSH agent replied with unexpected data, violating the SSH agent protocol.")] AgentProtocolError, - #[error("Agent failure")] + #[error( + "SSH agent replied with failure (protocol message number 5), which could not be handled." + )] AgentFailure, - #[error("Unable to connect to ssh-agent. The environment variable `SSH_AUTH_SOCK` was set, but it points to a nonexistent file or directory.")] - BadAuthSock, - #[error(transparent)] + #[error("Unable to connect to SSH agent because '{path}' was not found: {source}")] + BadAuthSock { + path: String, + source: std::io::Error, + }, + #[error("Encoding error while communicating with SSH agent: {0}")] Encoding(#[from] encoding::Error), - #[error("Environment variable `{0}` not found")] - EnvVar(&'static str), - #[error(transparent)] + #[error("Unable to read environment variable '{var}': {source}")] + EnvVar { + var: String, + source: std::env::VarError, + }, + #[error("I/O error while communicating with SSH agent: {0}")] Io(#[from] std::io::Error), - #[error(transparent)] - Private(Box), - #[error(transparent)] - Public(Box), - #[error(transparent)] - Signature(Box), } impl Error { pub fn is_not_running(&self) -> bool { - matches!(self, Self::EnvVar("SSH_AUTH_SOCK")) + matches!(self, Self::EnvVar { .. } | Self::BadAuthSock { .. }) } } /// SSH agent client. -pub struct AgentClient { +pub struct AgentClient { + /// The path that was originally used to connect to the agent. + path: Option, + + /// The underlying stream to the SSH agent. stream: S, } impl AgentClient { - /// Connect to an SSH agent via the provided stream (on Unix, usually a Unix-domain socket). - pub fn connect(stream: S) -> Self { - AgentClient { stream } - } - - /// Get the agent PID. - pub fn pid(&self) -> Option { - std::env::var("SSH_AGENT_PID") - .ok() - .and_then(|v| u32::from_str(&v).ok()) + pub fn path(&self) -> Option<&Path> { + self.path.as_deref() } } -pub trait ClientStream: Sized + Send + Sync { - /// Send an agent request through the stream and read the response. - fn request(&mut self, req: &[u8]) -> Result; - - /// How to connect the streaming socket - fn connect

(path: P) -> Result, Error> +impl AgentClient { + /// Connect to an SSH agent at the provided path. + pub fn connect

(path: P) -> Result where - P: AsRef + Send; + P: AsRef, + { + let path = path.as_ref().to_owned(); - fn connect_env() -> Result, Error> { - let Ok(var) = std::env::var("SSH_AUTH_SOCK") else { - return Err(Error::EnvVar("SSH_AUTH_SOCK")); - }; - match Self::connect(var) { - Err(Error::Io(io_err)) if io_err.kind() == std::io::ErrorKind::NotFound => { - Err(Error::BadAuthSock) + let stream = match Stream::connect(&path) { + Err(err) if err.kind() == std::io::ErrorKind::NotFound => { + return Err(Error::BadAuthSock { + path: path.display().to_string(), + source: err, + }) } - other => other, - } + Err(err) => return Err(Error::Io(err)), + Ok(stream) => stream, + }; + + Ok(Self { + path: Some(path), + stream, + }) + } + + pub fn connect_env() -> Result { + const SSH_AUTH_SOCK: &str = "SSH_AUTH_SOCK"; + + let path = match std::env::var(SSH_AUTH_SOCK) { + Ok(var) => var, + Err(err) => { + if cfg!(windows) { + // Windows uses a named pipe for the SSH agent, which + // we fall back to in case reading the environment + // variable fails. + "\\\\.\\pipe\\openssh-ssh-agent".to_string() + } else { + return Err(Error::EnvVar { + var: SSH_AUTH_SOCK.to_string(), + source: err, + }); + } + } + }; + + Self::connect(path) } } -impl AgentClient { +impl AgentClient { + pub fn new(path: Option, stream: Stream) -> Self { + Self { path, stream } + } + /// Send a key to the agent, with a (possibly empty) slice of constraints /// to apply when using the key to sign. pub fn add_identity(&mut self, key: &K, constraints: &[Constraint]) -> Result<(), Error> @@ -372,35 +405,11 @@ impl AgentClient { } } -#[cfg(not(unix))] -impl ClientStream for TcpStream { - fn connect_uds

(_: P) -> Result, Error> - where - P: AsRef + Send, - { - Err(Error::AgentFailure) - } - - fn read_response(&mut self, _: &mut Buffer) -> Result<(), Error> { - Err(Error::AgentFailure) - } - - fn connect_env() -> Result, Error> { - Err(Error::AgentFailure) - } +pub trait ClientStream: Sized + Send + Sync { + fn request(&mut self, msg: &[u8]) -> Result; } -#[cfg(unix)] -impl ClientStream for UnixStream { - fn connect

(path: P) -> Result, Error> - where - P: AsRef + Send, - { - let stream = UnixStream::connect(path)?; - - Ok(AgentClient { stream }) - } - +impl ClientStream for S { fn request(&mut self, msg: &[u8]) -> Result { let mut resp = Buffer::default();