Skip to main content

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::{Context, Result, anyhow};
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 made available
43    /// through the args. Otherwise, this function returns PAM_AUTH_ERR.
44    /// For the specifics of how the arguments are used to obtain ssh keys
45    /// and certificate authority keys, please refer to README.md
46    ///
47    /// This method logs diagnostic output to the AUTHPRIV syslog facility.
48    fn sm_authenticate(
49        pam_handle: &mut PamHandle,
50        args: Vec<&CStr>,
51        _flags: PamFlag,
52    ) -> PamResultCode {
53        match run(args, pam_handle) {
54            Ok(_) => {
55                debug!("Successful call to sm_authenticate(), returning PAM_SUCCESS");
56                PamResultCode::PAM_SUCCESS
57            }
58            Err(err) => {
59                for line in format!("{err:?}").split('\n') {
60                    error!("{line}")
61                }
62                debug!("Failed call to sm_authenticate(), returning PAM_AUTH_ERR");
63                PamResultCode::PAM_AUTH_ERR
64            }
65        }
66    }
67
68    // `doas` calls pam_setcred(), if this is not defined to succeed, it prints
69    // a fabulous `doas: pam_setcred(?, PAM_REINITIALIZE_CRED): Permission denied: Unknown error -3`
70    fn sm_setcred(
71        _pam_handle: &mut PamHandle,
72        _args: Vec<&CStr>,
73        _flags: PamFlag,
74    ) -> PamResultCode {
75        PamResultCode::PAM_SUCCESS
76    }
77}
78
79fn run(args: Vec<&CStr>, pam_handle: &PamHandle) -> Result<()> {
80    init_logging(pam_handle.get_service().unwrap_or("unknown".into()))?;
81    let args = Args::parse(args, &UnixEnvironment, pam_handle)?;
82    if args.debug {
83        log::set_max_level(log::LevelFilter::Debug);
84    }
85    do_authenticate(&args, pam_handle)?;
86    Ok(())
87}
88
89fn do_authenticate(args: &Args, handle: &PamHandle) -> Result<()> {
90    let path = get_path(args)?;
91    let calling_user = handle.get_calling_user()?;
92
93    info!("Authenticating user '{calling_user}' using ssh-agent at '{path}'");
94    if Path::new(&args.file).exists() {
95        info!("authorized keys from '{}'", &args.file);
96    }
97    if let Some(ca_keys_file) = &args.ca_keys_file {
98        info!("ca_keys from '{ca_keys_file}'");
99    };
100    if let Some(authorized_keys_command) = &args.authorized_keys_command {
101        info!("Invoking command '{authorized_keys_command}' to obtain keys");
102    }
103
104    let ssh_agent_client = Client::connect(Path::new(path.as_str()))?;
105
106    let filter = IdentityFilter::new(
107        Path::new(args.file.as_str()),
108        args.ca_keys_file.as_deref().map(Path::new),
109        args.authorized_keys_command.as_deref(),
110        args.authorized_keys_command_user.as_deref(),
111        &calling_user,
112    )?;
113
114    if check_sshd_special_case(handle.get_service().ok(), &filter, UnixEnvironment)? {
115        return Ok(());
116    }
117    match authenticate(&filter, ssh_agent_client, &handle.get_calling_user()?)? {
118        true => Ok(()),
119        false => Err(anyhow!("Agent did not know of any of the allowed keys")),
120    }
121}
122
123/// Returns true if SSH_SERVICE is sshd, and the environment variable SSH_AUTH_INFO_0 is set
124/// to a public key that filter is configured with.
125fn check_sshd_special_case(
126    service: Option<String>,
127    filter: &IdentityFilter,
128    env: impl Environment,
129) -> Result<bool> {
130    match service {
131        Some(service) => {
132            if service != "sshd" {
133                return Ok(false);
134            }
135        }
136        None => return Ok(false),
137    }
138    let Some(key) = env.get_env("SSH_AUTH_INFO_0") else {
139        debug!("calling service is sshd but SSH_AUTH_INFO_0 is not set");
140        return Ok(false);
141    };
142    Ok(filter.filter(
143        &PublicKey::from_openssh(&key)
144            .context("failed to parse key in SSH_AUTH_INFO_0 environment variable")?
145            .into(),
146    ))
147}
148
149fn get_path(args: &Args) -> Result<String> {
150    match env::var("SSH_AUTH_SOCK") {
151        Ok(path) => return Ok(path),
152        // It is not an error if this variable is not present, just continue down the function
153        Err(VarError::NotPresent) => {}
154        Err(_) => {
155            return Err(anyhow!("Failed to read environment variable SSH_AUTH_SOCK"));
156        }
157    }
158    match &args.default_ssh_auth_sock {
159        Some(path) => Ok(path.to_string()),
160        None => Err(anyhow!(
161            "SSH_AUTH_SOCK not set and the default_ssh_auth_sock parameter is not set"
162        )),
163    }
164}
165
166#[cfg(test)]
167mod tests {
168    use crate::check_sshd_special_case;
169    use crate::filter::IdentityFilter;
170    use crate::test::{CannedEnv, DummyEnv, data};
171    use anyhow::Result;
172    use std::path::Path;
173
174    #[test]
175    fn test_check_sshd_special_case() -> Result<()> {
176        let key = Path::new(data!("id_ed25519.pub"));
177        let filter = IdentityFilter::from_authorized_file(key)?;
178
179        // happy path, keys match
180        assert!(check_sshd_special_case(
181            Some("sshd".to_string()),
182            &filter,
183            CannedEnv::new(vec![include_str!(data!("id_ed25519.pub"))])
184        )?);
185
186        // different key
187        assert!(!check_sshd_special_case(
188            Some("sshd".to_string()),
189            &filter,
190            CannedEnv::new(vec![include_str!(data!("ca_key.pub"))])
191        )?);
192
193        // if service is not set, return false
194        assert!(!check_sshd_special_case(None, &filter, DummyEnv)?);
195
196        // if service is not set to something other than sshd, return false
197        assert!(!check_sshd_special_case(
198            Some("something".to_string()),
199            &filter,
200            DummyEnv
201        )?);
202
203        // not a key
204        assert!(
205            check_sshd_special_case(
206                Some("sshd".to_string()),
207                &filter,
208                CannedEnv::new(vec!["invalid"])
209            )
210            .is_err()
211        );
212
213        Ok(())
214    }
215}