pam_ssh_agent/
auth.rs

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