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 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 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
121fn 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 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 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 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 assert!(!check_sshd_special_case(None, &filter, DummyEnv)?);
193
194 assert!(!check_sshd_special_case(
196 Some("something".to_string()),
197 &filter,
198 DummyEnv
199 )?);
200
201 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}