pam_ssh_agent/
lib.rs

1mod agent;
2mod args;
3mod auth;
4mod cmd;
5mod environment;
6mod expansions;
7pub mod filter;
8mod logging;
9#[cfg(feature = "native-crypto")]
10mod nativecrypto;
11mod pamext;
12#[cfg(test)]
13mod test;
14mod verify;
15
16pub use crate::agent::SSHAgent;
17pub use crate::auth::authenticate;
18use pam::constants::{PamFlag, PamResultCode};
19use pam::module::{PamHandle, PamHooks};
20use std::env;
21use std::env::VarError;
22
23use crate::environment::{Environment, UnixEnvironment};
24use crate::filter::IdentityFilter;
25use crate::logging::init_logging;
26use crate::pamext::PamHandleExt;
27use anyhow::{anyhow, Context, Result};
28use args::Args;
29use log::{debug, error, info};
30use ssh_agent_client_rs::Client;
31use ssh_key::PublicKey;
32use std::ffi::CStr;
33use std::path::Path;
34
35struct PamSshAgent;
36pam::pam_hooks!(PamSshAgent);
37
38impl PamHooks for PamSshAgent {
39    /// The authentication method called by pam to authenticate the user. This method
40    /// will return PAM_SUCCESS if the ssh-agent available through the unix socket path
41    /// in the PAM_AUTH_SOCK environment variable is able to correctly sign a random
42    /// message with the private key corresponding to one of the public keys in
43    /// /etc/security/authorized_key. Otherwise, this function returns PAM_AUTH_ERR.
44    ///
45    /// This method logs diagnostic output to the AUTHPRIV syslog facility.
46    fn sm_authenticate(
47        pam_handle: &mut PamHandle,
48        args: Vec<&CStr>,
49        _flags: PamFlag,
50    ) -> PamResultCode {
51        match run(args, pam_handle) {
52            Ok(_) => {
53                debug!("Successful call to sm_authenticate(), returning PAM_SUCCESS");
54                PamResultCode::PAM_SUCCESS
55            }
56            Err(err) => {
57                for line in format!("{err:?}").split('\n') {
58                    error!("{line}")
59                }
60                debug!("Failed call to sm_authenticate(), returning PAM_AUTH_ERR");
61                PamResultCode::PAM_AUTH_ERR
62            }
63        }
64    }
65
66    // `doas` calls pam_setcred(), if this is not defined to succeed, it prints
67    // a fabulous `doas: pam_setcred(?, PAM_REINITIALIZE_CRED): Permission denied: Unknown error -3`
68    fn sm_setcred(
69        _pam_handle: &mut PamHandle,
70        _args: Vec<&CStr>,
71        _flags: PamFlag,
72    ) -> PamResultCode {
73        PamResultCode::PAM_SUCCESS
74    }
75}
76
77fn run(args: Vec<&CStr>, pam_handle: &PamHandle) -> Result<()> {
78    init_logging(pam_handle.get_service().unwrap_or("unknown".into()))?;
79    let args = Args::parse(args, &UnixEnvironment, pam_handle)?;
80    if args.debug {
81        log::set_max_level(log::LevelFilter::Debug);
82    }
83    do_authenticate(&args, pam_handle)?;
84    Ok(())
85}
86
87fn do_authenticate(args: &Args, handle: &PamHandle) -> Result<()> {
88    let path = get_path(args)?;
89    let calling_user = handle.get_calling_user()?;
90
91    info!("Authenticating user '{calling_user}' using ssh-agent at '{path}'");
92    if Path::new(&args.file).exists() {
93        info!("authorized keys from '{}'", &args.file);
94    }
95    if let Some(ca_keys_file) = &args.ca_keys_file {
96        info!("ca_keys from '{ca_keys_file}'");
97    };
98    if let Some(authorized_keys_command) = &args.authorized_keys_command {
99        info!("Invoking command '{authorized_keys_command}' to obtain keys");
100    }
101
102    let ssh_agent_client = Client::connect(Path::new(path.as_str()))?;
103
104    let filter = IdentityFilter::new(
105        Path::new(args.file.as_str()),
106        args.ca_keys_file.as_deref().map(Path::new),
107        args.authorized_keys_command.as_deref(),
108        args.authorized_keys_command_user.as_deref(),
109        &calling_user,
110    )?;
111
112    if check_sshd_special_case(handle.get_service().ok(), &filter, UnixEnvironment)? {
113        return Ok(());
114    }
115    match authenticate(&filter, ssh_agent_client, &handle.get_calling_user()?)? {
116        true => Ok(()),
117        false => Err(anyhow!("Agent did not know of any of the allowed keys")),
118    }
119}
120
121/// Returns true if SSH_SERVICE is sshd, and the environment variable SSH_AUTH_INFO_0 is set
122/// to a public key that filter is configured with.
123fn check_sshd_special_case(
124    service: Option<String>,
125    filter: &IdentityFilter,
126    env: impl Environment,
127) -> Result<bool> {
128    match service {
129        Some(service) => {
130            if service != "sshd" {
131                return Ok(false);
132            }
133        }
134        None => return Ok(false),
135    }
136    let Some(key) = env.get_env("SSH_AUTH_INFO_0") else {
137        debug!("calling service is sshd but SSH_AUTH_INFO_0 is not set");
138        return Ok(false);
139    };
140    Ok(filter.filter(
141        &PublicKey::from_openssh(&key)
142            .context("failed to parse key in SSH_AUTH_INFO_0 environment variable")?
143            .into(),
144    ))
145}
146
147fn get_path(args: &Args) -> Result<String> {
148    match env::var("SSH_AUTH_SOCK") {
149        Ok(path) => return Ok(path),
150        // It is not an error if this variable is not present, just continue down the function
151        Err(VarError::NotPresent) => {}
152        Err(_) => {
153            return Err(anyhow!("Failed to read environment variable SSH_AUTH_SOCK"));
154        }
155    }
156    match &args.default_ssh_auth_sock {
157        Some(path) => Ok(path.to_string()),
158        None => Err(anyhow!(
159            "SSH_AUTH_SOCK not set and the default_ssh_auth_sock parameter is not set"
160        )),
161    }
162}
163
164#[cfg(test)]
165mod tests {
166    use crate::check_sshd_special_case;
167    use crate::filter::IdentityFilter;
168    use crate::test::{data, CannedEnv, DummyEnv};
169    use anyhow::Result;
170    use std::path::Path;
171
172    #[test]
173    fn test_check_sshd_special_case() -> Result<()> {
174        let key = Path::new(data!("id_ed25519.pub"));
175        let filter = IdentityFilter::from_authorized_file(key)?;
176
177        // happy path, keys match
178        assert!(check_sshd_special_case(
179            Some("sshd".to_string()),
180            &filter,
181            CannedEnv::new(vec![include_str!(data!("id_ed25519.pub"))])
182        )?);
183
184        // different key
185        assert!(!check_sshd_special_case(
186            Some("sshd".to_string()),
187            &filter,
188            CannedEnv::new(vec![include_str!(data!("ca_key.pub"))])
189        )?);
190
191        // if service is not set, return false
192        assert!(!check_sshd_special_case(None, &filter, DummyEnv)?);
193
194        // if service is not set to something other than sshd, return false
195        assert!(!check_sshd_special_case(
196            Some("something".to_string()),
197            &filter,
198            DummyEnv
199        )?);
200
201        // not a key
202        assert!(check_sshd_special_case(
203            Some("sshd".to_string()),
204            &filter,
205            CannedEnv::new(vec!["invalid"])
206        )
207        .is_err());
208
209        Ok(())
210    }
211}