use ssh_key::{Certificate, Fingerprint, HashAlg, PublicKey};
use crate::Error;
#[allow(dead_code)]
#[derive(Debug, Clone)]
pub(crate) struct SshIdentity {
key: PublicKey,
}
#[allow(dead_code)]
impl SshIdentity {
pub(crate) fn public_key(&self) -> &PublicKey {
&self.key
}
pub(crate) fn comment(&self) -> &str {
self.key.comment()
}
pub(crate) fn fingerprint(&self) -> Fingerprint {
self.key.fingerprint(HashAlg::Sha256)
}
pub(crate) fn is_certificate(&self) -> bool {
self.key.algorithm().as_str().contains("-cert-")
}
pub(crate) fn as_certificate(&self) -> Option<Certificate> {
if !self.is_certificate() {
return None;
}
let bytes = self.key.to_bytes().ok()?;
Certificate::from_bytes(&bytes).ok()
}
}
#[allow(dead_code)]
pub(crate) fn connect() -> crate::Result<SshAgent> {
let sock_path = std::env::var("SSH_AUTH_SOCK")
.map_err(|_| Error::SshSign("SSH_AUTH_SOCK not set".into()))?;
let path = std::path::Path::new(&sock_path);
let client =
ssh_agent_client_rs::Client::connect(path).map_err(|e| Error::SshSign(e.to_string()))?;
Ok(SshAgent {
client: parking_lot::Mutex::new(client),
})
}
#[allow(dead_code)]
pub(crate) struct SshAgent {
client: parking_lot::Mutex<ssh_agent_client_rs::Client>,
}
#[allow(dead_code)]
impl SshAgent {
pub(crate) fn list_identities(&self) -> crate::Result<Vec<SshIdentity>> {
let mut client = self.client.lock();
let keys = client
.list_identities()
.map_err(|e| Error::SshSign(format!("failed to list identities: {e}")))?;
Ok(keys.into_iter().map(|key| SshIdentity { key }).collect())
}
pub(crate) fn sign_data(
&self,
identity: &SshIdentity,
data: &[u8],
) -> crate::Result<ssh_key::Signature> {
let mut client = self.client.lock();
let signature = client
.sign(identity.public_key(), data)
.map_err(|e| Error::SshSign(format!("failed to sign data: {e}")))?;
Ok(signature)
}
pub(crate) fn disconnect(self) -> crate::Result<()> {
drop(self.client);
Ok(())
}
pub(crate) fn find_cert_signer(&self, principal: &str) -> crate::Result<Option<SshIdentity>> {
let identities = self.list_identities()?;
for identity in &identities {
if let Some(cert) = identity.as_certificate()
&& cert.valid_principals().iter().any(|p| p == principal)
{
return Ok(Some(identity.clone()));
}
}
Ok(None)
}
pub(crate) fn find_pubkey_signer(
&self,
fingerprint: &str,
) -> crate::Result<Option<SshIdentity>> {
let identities = self.list_identities()?;
let normalized = fingerprint.strip_prefix("SHA256:").unwrap_or(fingerprint);
for identity in identities {
let key_fp = identity.fingerprint().to_string();
if key_fp == normalized || key_fp == fingerprint {
return Ok(Some(identity));
}
}
Ok(None)
}
}
#[cfg(test)]
mod tests {
use super::*;
struct SockGuard {
original: Option<String>,
}
impl SockGuard {
fn remove() -> Self {
let original = std::env::var("SSH_AUTH_SOCK").ok();
unsafe { std::env::remove_var("SSH_AUTH_SOCK") };
Self { original }
}
}
impl Drop for SockGuard {
fn drop(&mut self) {
if let Some(ref val) = self.original {
unsafe { std::env::set_var("SSH_AUTH_SOCK", val) };
}
}
}
#[test]
fn test_connect_missing_ssh_auth_sock() {
let _guard = SockGuard::remove();
let result = connect();
assert!(result.is_err(), "expected error when SSH_AUTH_SOCK not set");
let msg = result.err().expect("already asserted err").to_string();
assert!(
msg.contains("SSH_AUTH_SOCK not set"),
"expected 'SSH_AUTH_SOCK not set', got: {msg}"
);
}
#[test]
fn test_ssh_sign_error_type() {
let err = Error::SshSign("test error".into());
assert!(err.to_string().contains("SSH signing error"));
assert!(err.to_string().contains("test error"));
}
#[test]
fn test_find_cert_signer_no_agent() {
let _guard = SockGuard::remove();
let result = connect();
match result {
Err(e) => assert!(e.to_string().contains("SSH_AUTH_SOCK not set")),
Ok(agent) => {
let _ = agent.find_cert_signer("nonexistent-principal");
}
}
}
#[test]
fn test_find_pubkey_signer_no_agent() {
let _guard = SockGuard::remove();
let result = connect();
match result {
Err(e) => assert!(e.to_string().contains("SSH_AUTH_SOCK not set")),
Ok(agent) => {
let _ = agent.find_pubkey_signer("SHA256:nonexistent");
}
}
}
#[test]
fn test_ssh_identity_is_not_certificate_for_plain_key() {
let private_key = ssh_key::PrivateKey::from_openssh(include_str!(
"../../tests/ssh_fixtures/id_ed25519_test"
))
.expect("test key should parse");
let identity = SshIdentity {
key: private_key.public_key().clone(),
};
assert!(
!identity.is_certificate(),
"plain ed25519 key should not be a certificate"
);
assert!(identity.as_certificate().is_none());
assert!(
identity.fingerprint().to_string().starts_with("SHA256:"),
"fingerprint should start with SHA256:"
);
}
#[test]
fn test_find_pubkey_signer_empty_list() {
let _guard = SockGuard::remove();
let _ = connect();
}
}