use anyhow::{Context, Result};
use russh::keys::agent::AgentIdentity;
use russh::keys::agent::client::{AgentClient, AgentStream};
pub type Agent = AgentClient<Box<dyn AgentStream + Send + Unpin + 'static>>;
#[cfg(windows)]
const OPENSSH_PIPE: &str = r"\.\pipe\openssh-ssh-agent";
pub async fn connect() -> Result<Agent> {
#[cfg(unix)]
{
let socket = std::env::var("SSH_AUTH_SOCK")
.map_err(|_| anyhow::anyhow!("no SSH agent: SSH_AUTH_SOCK is not set - start one with `eval $(ssh-agent)`, then add the key with `ssh-add`"))?;
return AgentClient::connect_uds(&socket)
.await
.map(AgentClient::dynamic)
.with_context(|| format!("cannot reach the SSH agent at {socket}"));
}
#[cfg(windows)]
{
if let Ok(client) = AgentClient::connect_pageant().await {
return Ok(client.dynamic());
}
return AgentClient::connect_named_pipe(OPENSSH_PIPE)
.await
.map(AgentClient::dynamic)
.context(
"no SSH agent: neither Pageant nor the OpenSSH agent service answered - start the service with `Start-Service ssh-agent`, then add the key with `ssh-add`",
);
}
#[cfg(not(any(unix, windows)))]
anyhow::bail!("SSH agents are not supported on this platform")
}
pub async fn identities(agent: &mut Agent) -> Result<Vec<AgentIdentity>> {
agent.request_identities().await.context("cannot list the SSH agent's keys")
}
pub fn describe(identity: &AgentIdentity) -> String {
let key = identity.public_key();
let algorithm = key.algorithm().as_str().to_string();
let comment = identity.comment().trim();
if comment.is_empty() { algorithm } else { format!("{algorithm} {comment}") }
}
pub fn no_identities() -> anyhow::Error {
anyhow::anyhow!("the SSH agent is running but holds no keys - add one with `ssh-add PATH`")
}
#[cfg(test)]
mod tests {
use super::*;
use russh::keys::ssh_key::private::{Ed25519Keypair, KeypairData};
use russh::keys::{PrivateKey, PublicKey};
fn key(seed: u8, comment: &str) -> PrivateKey {
let pair = Ed25519Keypair::from_seed(&[seed; 32]);
PrivateKey::new(KeypairData::Ed25519(pair), comment).expect("an ed25519 key from a fixed seed")
}
fn identity(seed: u8, comment: &str) -> AgentIdentity {
let public: PublicKey = key(seed, comment).public_key().clone();
AgentIdentity::PublicKey {
key: public,
comment: comment.to_string(),
}
}
#[test]
fn an_identity_is_described_by_its_comment() {
assert_eq!(describe(&identity(1, "me@laptop")), "ssh-ed25519 me@laptop");
}
#[test]
fn an_identity_without_a_comment_falls_back_to_its_algorithm() {
assert_eq!(describe(&identity(2, "")), "ssh-ed25519");
assert_eq!(describe(&identity(3, " ")), "ssh-ed25519", "whitespace is not a comment");
}
#[test]
fn an_empty_agent_names_the_fix_on_this_machine() {
let message = no_identities().to_string();
assert!(message.contains("holds no keys"), "{message}");
assert!(message.contains("ssh-add"), "{message}");
}
}