Radish alpha
h
Radicle Heartwood Protocol & Stack
Radicle
Git (anonymous pull)
Log in to clone via SSH
ssh: Use winpipe for SSH agent on Windows
Lorenz Leutgeb committed 9 months ago
commit 009436efac8cd5b9a59cfe92688cda6f17d9531f
parent 0705501537d52d4bf1bf1a7480c3f25d98f164ad
5 files changed +101 -97
modified 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}")),
modified crates/radicle-crypto/src/ssh.rs
@@ -261,13 +261,6 @@ mod test {
    }

    impl ClientStream for DummyStream {
-
        fn connect<P>(_path: P) -> Result<AgentClient<Self>, Error>
-
        where
-
            P: AsRef<std::path::Path> + Send,
-
        {
-
            panic!("This function should never be called!")
-
        }
-

        fn request(&mut self, buf: &[u8]) -> Result<Buffer, Error> {
            *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<u8> = vec![1, 2, 3, 4, 5, 6, 7, 8, 9];

        agent.sign(&pk, &data).ok();
modified 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<Stream>,
+
    client: AgentClient,
}

impl Agent {
    /// Connect to a running SSH agent.
-
    pub fn connect() -> Result<Self, ssh::agent::client::Error> {
-
        Stream::connect_env().map(|client| Self { client })
+
    pub fn connect() -> Result<Self, Error> {
+
        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<u32> {
-
        self.client.pid()
+
    pub fn path(&self) -> Option<&Path> {
+
        self.client.path()
    }

    pub fn request_identities(&mut self) -> Result<Vec<PublicKey>, ssh::agent::client::Error> {
modified 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
modified 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<dyn std::error::Error + Send + Sync + 'static>),
-
    #[error(transparent)]
-
    Public(Box<dyn std::error::Error + Send + Sync + 'static>),
-
    #[error(transparent)]
-
    Signature(Box<dyn std::error::Error + Send + Sync + 'static>),
}

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<S> {
+
pub struct AgentClient<S = Stream> {
+
    /// The path that was originally used to connect to the agent.
+
    path: Option<PathBuf>,
+

+
    /// The underlying stream to the SSH agent.
    stream: S,
}

impl<S> AgentClient<S> {
-
    /// 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<u32> {
-
        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<Buffer, Error>;
-

-
    /// How to connect the streaming socket
-
    fn connect<P>(path: P) -> Result<AgentClient<Self>, Error>
+
impl AgentClient<Stream> {
+
    /// Connect to an SSH agent at the provided path.
+
    pub fn connect<P>(path: P) -> Result<Self, Error>
    where
-
        P: AsRef<Path> + Send;
-

-
    fn connect_env() -> Result<AgentClient<Self>, Error> {
-
        let Ok(var) = std::env::var("SSH_AUTH_SOCK") else {
-
            return Err(Error::EnvVar("SSH_AUTH_SOCK"));
+
        P: AsRef<Path>,
+
    {
+
        let path = path.as_ref().to_owned();
+

+
        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,
+
                })
+
            }
+
            Err(err) => return Err(Error::Io(err)),
+
            Ok(stream) => stream,
        };
-
        match Self::connect(var) {
-
            Err(Error::Io(io_err)) if io_err.kind() == std::io::ErrorKind::NotFound => {
-
                Err(Error::BadAuthSock)
+

+
        Ok(Self {
+
            path: Some(path),
+
            stream,
+
        })
+
    }
+

+
    pub fn connect_env() -> Result<Self, Error> {
+
        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,
+
                    });
+
                }
            }
-
            other => other,
-
        }
+
        };
+

+
        Self::connect(path)
    }
}

-
impl<S: ClientStream> AgentClient<S> {
+
impl<Stream: ClientStream> AgentClient<Stream> {
+
    pub fn new(path: Option<PathBuf>, 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<K>(&mut self, key: &K, constraints: &[Constraint]) -> Result<(), Error>
@@ -372,35 +405,11 @@ impl<S: ClientStream> AgentClient<S> {
    }
}

-
#[cfg(not(unix))]
-
impl ClientStream for TcpStream {
-
    fn connect_uds<P>(_: P) -> Result<AgentClient<Self>, Error>
-
    where
-
        P: AsRef<Path> + Send,
-
    {
-
        Err(Error::AgentFailure)
-
    }
-

-
    fn read_response(&mut self, _: &mut Buffer) -> Result<(), Error> {
-
        Err(Error::AgentFailure)
-
    }
-

-
    fn connect_env() -> Result<AgentClient<Self>, Error> {
-
        Err(Error::AgentFailure)
-
    }
+
pub trait ClientStream: Sized + Send + Sync {
+
    fn request(&mut self, msg: &[u8]) -> Result<Buffer, Error>;
}

-
#[cfg(unix)]
-
impl ClientStream for UnixStream {
-
    fn connect<P>(path: P) -> Result<AgentClient<Self>, Error>
-
    where
-
        P: AsRef<Path> + Send,
-
    {
-
        let stream = UnixStream::connect(path)?;
-

-
        Ok(AgentClient { stream })
-
    }
-

+
impl<S: Read + Write + Sized + Send + Sync> ClientStream for S {
    fn request(&mut self, msg: &[u8]) -> Result<Buffer, Error> {
        let mut resp = Buffer::default();