ssh_agent_client_rs/
lib.rs

1//! # ssh-agent-client-rs
2//!
3//! An ssh-agent client implementation in rust, aiming to provide a robust,
4//! well tested and easy to use synchronous API to interact with an ssh-agent.
5//!
6//! # Examples
7//! ```no_run
8//! use ssh_agent_client_rs::Client;
9//! # use std::env;
10//! # use std::path::Path;
11//! # use ssh_agent_client_rs::{Identity, Error};
12//! use ssh_key::PublicKey;
13//!
14//! # let env = env::var("SSH_AUTH_SOCK").unwrap();
15//! # let path_to_ssh_auth_socket = Path::new(env.as_str());
16//! let mut client = Client::connect(path_to_ssh_auth_socket).expect("failed to connect");
17//!
18//! // List the identities that the connected ssh-agent makes available
19//! let identities: Vec<Identity> = client.list_all_identities().expect("failed to list identities");
20//! ```
21
22use crate::codec::{read_message, write_message, ReadMessage, WriteMessage};
23#[cfg(target_family = "windows")]
24use interprocess::os::windows::named_pipe::{pipe_mode, DuplexPipeStream};
25use ssh_key::public::KeyData;
26use ssh_key::{Certificate, PrivateKey, PublicKey, Signature};
27use std::borrow::Cow;
28use std::io::{Read, Write};
29#[cfg(target_family = "unix")]
30use std::os::unix::net::UnixStream;
31use std::path::Path;
32
33mod codec;
34mod error;
35
36pub use self::error::Error;
37pub use self::error::Result;
38
39/// A combination of the std::io::Read and std::io::Write traits.
40pub trait ReadWrite: Read + Write {}
41
42/// Blanket implementation of ReadWrite for all types that implement both Read and Write.
43/// This makes things such as sockets and files compatible with the ReadWrite abstraction
44impl<T> ReadWrite for T where T: Read + Write {}
45
46/// A Client instance is an object that can be used to interact with an ssh-agent,
47/// typically using a Unix socket
48pub struct Client {
49    socket: Box<dyn ReadWrite>,
50}
51
52#[derive(Debug, PartialEq, Clone)]
53pub enum Identity<'a> {
54    PublicKey(Box<Cow<'a, PublicKey>>),
55    Certificate(Box<Cow<'a, Certificate>>),
56}
57
58impl<'a> From<PublicKey> for Identity<'a> {
59    fn from(value: PublicKey) -> Self {
60        Identity::PublicKey(Box::new(Cow::Owned(value)))
61    }
62}
63
64impl<'a> From<&'a PublicKey> for Identity<'a> {
65    fn from(value: &'a PublicKey) -> Self {
66        Identity::PublicKey(Box::new(Cow::Borrowed(value)))
67    }
68}
69
70impl<'a> From<Certificate> for Identity<'a> {
71    fn from(value: Certificate) -> Self {
72        Identity::Certificate(Box::new(Cow::Owned(value)))
73    }
74}
75
76impl<'a> From<&'a Certificate> for Identity<'a> {
77    fn from(value: &'a Certificate) -> Self {
78        Identity::Certificate(Box::new(Cow::Borrowed(value)))
79    }
80}
81
82impl<'a> From<&'a Identity<'a>> for &'a KeyData {
83    fn from(value: &'a Identity) -> Self {
84        match value {
85            Identity::PublicKey(pk) => pk.key_data(),
86            Identity::Certificate(cert) => cert.public_key(),
87        }
88    }
89}
90
91impl<'a> Client {
92    /// Constructs a Client connected to a unix socket referenced by path.
93    #[cfg(target_family = "unix")]
94    pub fn connect(path: &Path) -> Result<Client> {
95        let socket = Box::new(UnixStream::connect(path)?);
96        Ok(Client { socket })
97    }
98
99    // If you want to communicate with the ssh-agent shipped with windows you probably want to pass
100    // Path::new(r"\\.\pipe\openssh-ssh-agent")
101    #[cfg(target_family = "windows")]
102    pub fn connect(path: &Path) -> Result<Client> {
103        let pipe = DuplexPipeStream::<pipe_mode::Bytes>::connect_by_path(path)?;
104        Ok(Client {
105            socket: Box::new(pipe),
106        })
107    }
108
109    /// Construct a Client backed by an implementation of ReadWrite, mainly useful for
110    /// testing.
111    pub fn with_read_write(read_write: Box<dyn ReadWrite>) -> Client {
112        Client { socket: read_write }
113    }
114
115    /// List the identities that has been added to the connected ssh-agent. Identities that
116    /// are not ssh public keys, particularly identities that corresponds to certs, are ignored
117    #[deprecated(note = "Use list_all_identities() instead")]
118    pub fn list_identities(&mut self) -> Result<Vec<PublicKey>> {
119        self.list_all_identities().map(|identities| {
120            identities
121                .into_iter()
122                .filter_map(|i| match i {
123                    Identity::PublicKey(pk) => Some(pk.into_owned()),
124                    _ => None,
125                })
126                .collect()
127        })
128    }
129    /// List the identities that have been added to the connected ssh-agent including certs.
130    pub fn list_all_identities(&mut self) -> Result<Vec<Identity<'static>>> {
131        write_message(&mut self.socket, WriteMessage::RequestIdentities)?;
132        match read_message(&mut self.socket)? {
133            ReadMessage::Identities(identities) => Ok(identities),
134            m => Err(unexpected_response(m)),
135        }
136    }
137
138    /// Add an identity to the connected ssh-agent.
139    pub fn add_identity(&mut self, key: &PrivateKey) -> Result<()> {
140        write_message(&mut self.socket, WriteMessage::AddIdentity(key))?;
141        self.expect_success()
142    }
143
144    /// Remove an identity from the connected ssh-agent.
145    pub fn remove_identity(&mut self, key: &PrivateKey) -> Result<()> {
146        write_message(&mut self.socket, WriteMessage::RemoveIdentity(key))?;
147        self.expect_success()
148    }
149
150    /// Remove all identities from the connected ssh-agent.
151    pub fn remove_all_identities(&mut self) -> Result<()> {
152        write_message(&mut self.socket, WriteMessage::RemoveAllIdentities)?;
153        self.expect_success()
154    }
155
156    /// Instruct the connected ssh-agent to sign data with the private key associated with the
157    /// provided public key. For now, sign requests with RSA keys are hard coded to use the
158    /// SHA-512 hashing algorithm.
159    pub fn sign(&mut self, key: impl Into<Identity<'a>>, data: &[u8]) -> Result<Signature> {
160        self.sign_with_ref(&key.into(), data)
161    }
162    pub fn sign_with_ref(&mut self, identity: &Identity, data: &[u8]) -> Result<Signature> {
163        write_message(&mut self.socket, WriteMessage::Sign(identity, data))?;
164        match read_message(&mut self.socket)? {
165            ReadMessage::Signature(sig) => Ok(sig),
166            ReadMessage::Failure => Err(Error::RemoteFailure),
167            m => Err(unexpected_response(m)),
168        }
169    }
170
171    fn expect_success(&mut self) -> Result<()> {
172        let response = read_message(&mut self.socket)?;
173        match response {
174            ReadMessage::Success => Ok(()),
175            ReadMessage::Failure => Err(Error::RemoteFailure),
176            _ => Err(Error::InvalidMessage("Unexpected response".to_string())),
177        }
178    }
179}
180
181fn unexpected_response(message: ReadMessage) -> Error {
182    let error = format!("Agent responded with unexpected message '{message:?}'");
183    Error::InvalidMessage(error)
184}