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