pam_ssh_agent/
auth.rs

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
13/// Finds the first key, if any, that the ssh-agent knows about that is also present
14/// in the file referenced by keys_file_path, sends a random message to be signed and
15/// verifies the signature with the public key.
16///
17/// Returns Ok(true) if a key was found and the signature was correct, Ok(false) if no
18/// key was found, and Err if agent communication or signature verification failed.
19pub 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            // Allow sign_and_verify() to return RemoteFailure (key not loaded / present),
32            // and try the next configured key
33            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}