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();