1pub use crate::agent::SSHAgent;
2use crate::log::Log;
3use anyhow::{anyhow, Context, Result};
4use signature::Verifier;
5use ssh_agent_client_rs::Error as SACError;
6use ssh_key::public::KeyData;
7use ssh_key::{AuthorizedKeys, PublicKey};
8use std::collections::HashSet;
9use std::path::Path;
10
11const CHALLENGE_SIZE: usize = 32;
12
13pub fn authenticate(
20 keys_file_path: &str,
21 mut agent: impl SSHAgent,
22 log: &mut impl Log,
23) -> Result<bool> {
24 let keys = keys_from_file(Path::new(keys_file_path))?;
25 for key in agent.list_identities()? {
26 if keys.contains(key.key_data()) {
27 log.debug(format!(
28 "found a matching key: {}",
29 key.fingerprint(Default::default())
30 ))?;
31 match sign_and_verify(&key, &mut agent) {
34 Ok(res) => return Ok(res),
35 Err(e) => {
36 if let Some(SACError::RemoteFailure) = e.downcast_ref::<SACError>() {
37 log.debug("SSHAgent: RemoteFailure; trying next key")?;
38 continue;
39 } else {
40 return Err(e);
41 }
42 }
43 }
44 }
45 }
46 Ok(false)
47}
48
49fn sign_and_verify(key: &PublicKey, agent: &mut impl SSHAgent) -> Result<bool> {
50 let mut data: [u8; CHALLENGE_SIZE] = [0_u8; CHALLENGE_SIZE];
51 getrandom::fill(data.as_mut_slice()).map_err(|_| anyhow!("Failed to obtain random data"))?;
52 let sig = agent.sign(key, data.as_ref())?;
53
54 key.key_data().verify(data.as_ref(), &sig)?;
55 Ok(true)
56}
57
58fn keys_from_file(path: &Path) -> Result<HashSet<KeyData>> {
59 Ok(HashSet::from_iter(
60 AuthorizedKeys::read_file(path)
61 .context(format!("Failed to read from {:?}", path))?
62 .into_iter()
63 .map(|e| e.public_key().key_data().to_owned()),
64 ))
65}
66
67#[cfg(test)]
68mod test {
69 use crate::auth::keys_from_file;
70 use ssh_key::PublicKey;
71 use std::path::Path;
72
73 const KEY_FROM_AUTHORIZED_KEYS: &str = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIObUcR\
74 y1Nv6fz4xnAXqOaFL/A+gGM9OF+l2qpsDPmMlU test@ed25519";
75
76 const ANOTHER_KEY: &str = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIMdtbb2fnK02RReYsJW\
77 jh1F2q102dIer60vbgj+cABcO noa@Noas-Laptop.local";
78
79 #[test]
80 fn test_read_public_keys() {
81 let path = Path::new(concat!(
82 env!("CARGO_MANIFEST_DIR"),
83 "/tests/data/authorized_keys"
84 ));
85
86 let result = keys_from_file(path).expect("Failed to parse");
87
88 let key = PublicKey::from_openssh(KEY_FROM_AUTHORIZED_KEYS).unwrap();
89 assert!(result.contains(key.key_data()));
90
91 let key = PublicKey::from_openssh(ANOTHER_KEY).unwrap();
92 assert!(!result.contains(key.key_data()));
93 }
94}