pam_ssh_agent/
auth.rs

1pub use crate::agent::SSHAgent;
2use crate::filter::IdentityFilter;
3use crate::verify::verify;
4use anyhow::{anyhow, Result};
5use log::{debug, info};
6use ssh_agent_client_rs::{Error as SACError, Identity};
7use ssh_key::HashAlg;
8use std::time::{SystemTime, UNIX_EPOCH};
9use Identity::{Certificate, PublicKey};
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    filter: &IdentityFilter,
21    mut agent: impl SSHAgent,
22    principal: &str,
23) -> Result<bool> {
24    for identity in agent.list_identities()? {
25        if filter.filter(&identity) {
26            if let Certificate(cert) = &identity {
27                if !validate_cert(cert, SystemTime::now(), principal) {
28                    info!("Cert not valid, skipping");
29                    continue;
30                }
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(identity, &mut agent) {
35                Ok(res) => return Ok(res),
36                Err(e) => {
37                    if let Some(SACError::RemoteFailure) = e.downcast_ref::<SACError>() {
38                        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(identity: Identity<'static>, agent: &mut impl SSHAgent) -> Result<bool> {
51    let mut data: [u8; CHALLENGE_SIZE] = [0_u8; CHALLENGE_SIZE];
52    getrandom::fill(data.as_mut_slice()).map_err(|_| anyhow!("Failed to obtain random data"))?;
53    let sig = agent.sign(identity.clone(), &data)?;
54    match identity {
55        PublicKey(key) => verify(key.key_data(), &data, &sig)?,
56        Certificate(cert) => verify(cert.public_key(), &data, &sig)?,
57    };
58    Ok(true)
59}
60
61fn validate_cert(cert: &ssh_key::Certificate, when: SystemTime, principal: &str) -> bool {
62    let ca_key = cert.signature_key();
63
64    if let Err(e) = cert.validate_at(
65        when.duration_since(UNIX_EPOCH)
66            .expect("Time went backwards")
67            .as_secs(),
68        vec![&ca_key.fingerprint(HashAlg::Sha256)],
69    ) {
70        info!("Certificate validation failed: {e:?}");
71        return false;
72    }
73
74    if !cert.valid_principals().iter().any(|p| p == principal) {
75        info!("Cert matches but '{principal}' is not in the list of valid principals.");
76        return false;
77    }
78
79    if !cert.critical_options().is_empty() {
80        info!("Cert has critical options we don't know how to handle");
81        return false;
82    }
83
84    true
85}
86
87#[cfg(test)]
88mod test {
89    use crate::auth::validate_cert;
90    use crate::test::{data, CERT_STR};
91    use anyhow::Result;
92    use ssh_key::Certificate;
93    use std::time::{Duration, SystemTime};
94
95    #[test]
96    fn test_parse_cert() -> Result<()> {
97        let cert = Certificate::from_openssh(CERT_STR)?;
98        // within validity: 2025-07-15 12:00:00
99        assert!(validate_cert(&cert, st(1752577200), "principal"));
100        // wrong principal
101        assert!(!validate_cert(&cert, st(1752577200), "another"));
102        // too early: 2025-06-15 12:00:00
103        assert!(!validate_cert(&cert, st(1749985200), "principal"));
104        // too late: 2025-08-15 12:00:00
105        assert!(!validate_cert(&cert, st(1755255600), "principal"));
106
107        // let's change a byte and check if the signature verification fails
108        let mut bytes = CERT_STR.as_bytes().to_vec();
109        bytes[90] = 0x42;
110        let cert = Certificate::from_openssh(&String::from_utf8_lossy(bytes.as_slice()))?;
111        // within validity: 2025-07-15 12:00:00 but the data is scrambled
112        assert!(!validate_cert(&cert, st(1752577200), "principal"));
113
114        Ok(())
115    }
116
117    #[test]
118    fn test_unknown_critical_field_in_cert() -> Result<()> {
119        let cert = Certificate::from_openssh(include_str!(data!("cert_unknown_critical.pub")))?;
120        // within validity: 1999-08-15 12:00:00
121        assert!(!validate_cert(&cert, st(934714800), "user"));
122        Ok(())
123    }
124
125    fn st(timestamp: u64) -> SystemTime {
126        SystemTime::UNIX_EPOCH + Duration::from_secs(timestamp)
127    }
128}