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