From 938e1e28294bb66a328c788d259341b34f23f2a4 Mon Sep 17 00:00:00 2001 From: z4yx Date: Mon, 27 Nov 2023 01:02:47 +0800 Subject: [PATCH 1/4] add the support of variable substitution in options --- Cargo.lock | 23 ++++++++++++++ Cargo.toml | 1 + src/error.rs | 8 +++++ src/lib.rs | 81 +++++++++++++++++++++++++++++------------------- src/pam_items.rs | 50 ++++++++++++++++++++++++++++++ 5 files changed, 131 insertions(+), 32 deletions(-) create mode 100644 src/pam_items.rs diff --git a/Cargo.lock b/Cargo.lock index 9b7759e..7a23ba7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -101,6 +101,12 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ffbee8634e0d45d258acb448e7eaab3fce7a0a467395d4d9f228e3c1f01fb2e4" +[[package]] +name = "memchr" +version = "2.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f665ee40bc4a3c5590afb1e9677db74a508659dfd71e126420da8274909a0167" + [[package]] name = "multisock" version = "1.0.0" @@ -182,6 +188,7 @@ dependencies = [ "pam-bindings", "pwd", "ssh-agent", + "subst", "syslog", ] @@ -249,6 +256,16 @@ dependencies = [ "serde", ] +[[package]] +name = "subst" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca1318e5d6716d6541696727c88d9b8dfc8cfe6afd6908e186546fd4af7f5b98" +dependencies = [ + "memchr", + "unicode-width", +] + [[package]] name = "syn" version = "1.0.109" @@ -339,6 +356,12 @@ version = "1.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5464a87b239f13a63a501f2701565754bae92d243d4bb7eb12f6d57d2269bf4" +[[package]] +name = "unicode-width" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85" + [[package]] name = "vcpkg" version = "0.2.15" diff --git a/Cargo.toml b/Cargo.toml index 1d4f1b9..41143c7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,5 +17,6 @@ base64 = "^0.13.0" openssl-sys = "^0.9" openssl = "^0.10" log = { version = "^0.4", features = ["std", "serde"] } +subst = "^0.3.0" syslog = "^6.0" pwd = "1" diff --git a/src/error.rs b/src/error.rs index 3580879..5824425 100644 --- a/src/error.rs +++ b/src/error.rs @@ -16,6 +16,10 @@ pub enum RsshErr { GetUidErr, CmdExitErr(Option), CmdOutputDecodeErr, + InvalidLogLvlErr, + OptNameErr(String), + OptValEmptyErr(String), + OptVarErr(String), } impl RsshErr { @@ -45,6 +49,10 @@ impl Display for RsshErr { RsshErr::GetUidErr => S!("Cannot get uid of specified user"), RsshErr::CmdExitErr(code) => format!("Command exit code is {}", code.unwrap_or(-1)), RsshErr::CmdOutputDecodeErr => S!("Failed to decode the output of command"), + RsshErr::InvalidLogLvlErr => S!("Invalid log level"), + RsshErr::OptNameErr(name) => format!("Unknown option name `{}`", name), + RsshErr::OptValEmptyErr(name) => format!("Value of option `{}` is empty", name), + RsshErr::OptVarErr(name) => format!("Failed to evaluate variables in option `{}`", name), }; f.write_str(&msg) } diff --git a/src/lib.rs b/src/lib.rs index a341e04..75bd86b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -6,6 +6,7 @@ mod error; mod logger; mod sign_verify; mod ssh_agent_auth; +mod pam_items; use log::*; use pam::constants::{PamFlag, PamResultCode}; @@ -82,6 +83,18 @@ fn setup_logger() { .map(|()| log::set_max_level(log::LevelFilter::Warn)); } +fn substitute_variables(kv: &Vec<&str>, variables: &pam_items::PamItemsMap) -> Result { + subst::substitute(kv[1], variables) + .or(Err(RsshErr::OptVarErr(kv[0].to_string()).into_ptr())) +} + +fn non_empty_option_check(kv: &Vec<&str>) -> Result<(), ErrType> { + if kv.len() == 1 || kv[1].is_empty() { + return Err(RsshErr::OptValEmptyErr(kv[0].to_string()).into_ptr()) + } + Ok(()) +} + impl PamHooks for PamRssh { fn sm_authenticate(pamh: &mut PamHandle, args: Vec<&CStr>, _flags: PamFlag) -> PamResultCode { /* if (flags & pam::constants::PAM_SILENT) == 0 */ @@ -89,48 +102,52 @@ impl PamHooks for PamRssh { setup_logger(); } - let mut ssh_agent_addr = ""; - let mut auth_key_file = ""; - let mut authorized_keys_command = ""; - let mut authorized_keys_command_user = ""; + let pam_vars = pam_items::PamItemsMap::new(&pamh); + let mut ssh_agent_addr = String::new(); + let mut auth_key_file = String::new(); + let mut authorized_keys_command = String::new(); + let mut authorized_keys_command_user = String::new(); for carg in args { let kv: Vec<&str> = carg.to_str().unwrap_or("").splitn(2, '=').collect(); if kv.len() == 0 { continue; } trace!("Parsing option {:?}", kv); - match kv[0] { - "loglevel" => { - if kv.len() > 1 { - let _ = log::Level::from_str(kv[1]) - .and_then(|level| Ok(log::set_max_level(level.to_level_filter()))); + let mut parse_options = || -> Result<(), ErrType> { + match kv[0] { + "loglevel" => { + non_empty_option_check(&kv)?; + match log::Level::from_str(kv[1]) { + Ok(level) => log::set_max_level(level.to_level_filter()), + Err(_) => { return Err(RsshErr::InvalidLogLvlErr.into_ptr()) } + } } - } - "debug" => log::set_max_level(log::LevelFilter::Debug), - "ssh_agent_addr" => { - if kv.len() > 1 { - ssh_agent_addr = kv[1]; + "debug" => log::set_max_level(log::LevelFilter::Debug), + "ssh_agent_addr" => { + non_empty_option_check(&kv)?; + ssh_agent_addr = substitute_variables(&kv, &pam_vars)?; } - } - "auth_key_file" => { - if kv.len() > 1 { - auth_key_file = kv[1]; + "auth_key_file" => { + non_empty_option_check(&kv)?; + auth_key_file = substitute_variables(&kv, &pam_vars)?; } - } - "authorized_keys_command" => { - if kv.len() > 1 { - authorized_keys_command = kv[1]; + "authorized_keys_command" => { + non_empty_option_check(&kv)?; + authorized_keys_command = substitute_variables(&kv, &pam_vars)?; } - } - "authorized_keys_command_user" => { - if kv.len() > 1 { - authorized_keys_command_user = kv[1]; + "authorized_keys_command_user" => { + non_empty_option_check(&kv)?; + authorized_keys_command_user = substitute_variables(&kv, &pam_vars)?; + } + _ => { + return Err(RsshErr::OptNameErr(kv[0].to_string()).into_ptr()); } } - _ => { - error!("Unknown option `{}`", kv[0]); - return PamResultCode::PAM_SYSTEM_ERR; - } + Ok(()) + }; + if let Err(opt_err) = parse_options() { + error!("{}", opt_err); + return PamResultCode::PAM_SYSTEM_ERR; } } @@ -139,7 +156,7 @@ impl PamHooks for PamRssh { let agent_addr_os = std::env::var_os("SSH_AUTH_SOCK"); if let Some(a) = agent_addr_os { addr_from_env = a; - ssh_agent_addr = addr_from_env.to_str().unwrap_or(""); + ssh_agent_addr = addr_from_env.to_str().unwrap_or("").to_string(); } debug!("SSH-Agent address: {}", ssh_agent_addr); if ssh_agent_addr.is_empty() { @@ -164,7 +181,7 @@ impl PamHooks for PamRssh { } }; - let mut agent = ssh_agent_auth::AgentClient::new(ssh_agent_addr); + let mut agent = ssh_agent_auth::AgentClient::new(ssh_agent_addr.as_str()); let result = agent.list_identities().and_then(|client_keys| { debug!("SSH-Agent reports {} keys", client_keys.len()); for (i, key) in client_keys.iter().enumerate() { diff --git a/src/pam_items.rs b/src/pam_items.rs new file mode 100644 index 0000000..3576ec5 --- /dev/null +++ b/src/pam_items.rs @@ -0,0 +1,50 @@ + +use pam::constants::PamResultCode; +use pam::module::{PamHandle, PamResult}; +use pam::items; +use subst::VariableMap; + +pub struct PamItemsMap<'a> { + pam: &'a PamHandle, +} + +impl<'a> VariableMap<'a> for PamItemsMap<'a> { + type Value = String; + fn get(&'a self, key: &str) -> Option { + self.get_str_val(key).ok() + } +} + +impl<'a> PamItemsMap<'a> { + pub fn new(pam_handle: &PamHandle) ->PamItemsMap { + PamItemsMap { pam: pam_handle } + } + fn get_str_val(&'a self, key: &str) -> PamResult { + let str_val = match key { + "service" => { + let v = self.pam.get_item::()?.ok_or(PamResultCode::PAM_BUF_ERR)?; + v.to_str().or(Err(PamResultCode::PAM_BUF_ERR))? + } + "user" => { + let v = self.pam.get_item::()?.ok_or(PamResultCode::PAM_BUF_ERR)?; + v.to_str().or(Err(PamResultCode::PAM_BUF_ERR))? + } + "tty" => { + let v = self.pam.get_item::()?.ok_or(PamResultCode::PAM_BUF_ERR)?; + v.to_str().or(Err(PamResultCode::PAM_BUF_ERR))? + } + "rhost" => { + let v = self.pam.get_item::()?.ok_or(PamResultCode::PAM_BUF_ERR)?; + v.to_str().or(Err(PamResultCode::PAM_BUF_ERR))? + } + "ruser" => { + let v = self.pam.get_item::()?.ok_or(PamResultCode::PAM_BUF_ERR)?; + v.to_str().or(Err(PamResultCode::PAM_BUF_ERR))? + } + _ => { + return Err(PamResultCode::PAM_BAD_ITEM) + } + }; + Ok(str_val.to_string()) + } +} From 347fa76a3394d04fc501b271f465c75373e52dc1 Mon Sep 17 00:00:00 2001 From: z4yx Date: Mon, 27 Nov 2023 18:46:12 +0800 Subject: [PATCH 2/4] document the variable in options --- README.md | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/README.md b/README.md index d2b504b..18bdfbf 100644 --- a/README.md +++ b/README.md @@ -83,3 +83,21 @@ Arguments should be appended to the PAM rule. For example: ``` auth sufficient libpam_rssh.so debug authorized_keys_command=/usr/bin/sss_ssh_authorizedkeys authorized_keys_command_user=nobody ``` + +## Use Variables in Arguments + +Certain variables can be used in arguments. Supported formats are `$var`, `${var}` and `${var:default value}`. For example: + +``` +auth sufficient libpam_rssh.so auth_key_file=/data/${user}.keys +``` + +Variables are mapped to PAM items. Currently the following variables are available: + +- service: PAM_SERVICE. The service name (which identifies the PAM stack that will be used). +- user: PAM_USER. The username of the entity under whose identity service will be given. +- tty: PAM_TTY. The terminal name. +- rhost: PAM_RHOST. The requesting hostname. +- ruser: PAM_RUSER. The requesting entity. + +For detailed description on PAM items, read man page pam_get_item(3). From ae370d2610f1abbd993f67dcc653cd7212a037f8 Mon Sep 17 00:00:00 2001 From: z4yx Date: Mon, 27 Nov 2023 18:47:48 +0800 Subject: [PATCH 3/4] refine README --- README.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 18bdfbf..1e0ee95 100644 --- a/README.md +++ b/README.md @@ -41,7 +41,8 @@ cp target/release/libpam_rssh.so ``` ## `pam module path` -- the module path is specific to certain distributions + +The module path is specific to certain distributions | OS | Destination | | ------------ | ----------------------------------- | @@ -74,7 +75,7 @@ The following arguments are supported: - `loglevel=` Select the level of messages logged to syslog. Defaults to `warn`. - `debug` Equivalent to `loglevel=debug`. - `ssh_agent_addr=` The address of ssh-agent. Defaults to the value of `SSH_AUTH_SOCK` environment variable, which is set by ssh automatically. -- `auth_key_file=` Public keys allowed for user authentication. Defaults to `$HOME/.ssh/authorized_keys`. Usually `$HOME` expands to `/home/`. +- `auth_key_file=` Public keys allowed for user authentication. Defaults to `/.ssh/authorized_keys`. `` is read from system configuration, usually it expands to `/home/`. - `authorized_keys_command=` A command to generate the authorized_keys. It takes a single argument, the username of the user being authenticated. The standard output of this command will be parsed as authorized_keys. The `auth_key_file` will be ignored if you specify this argument. - `authorized_keys_command_user=` The `authorized_keys_command` will be run as the user specified here. If this argument is omitted, the `authorized_keys_command` will be run as the user being authenticated. From ca8722e71cc7a3a2aa9ac1512139e8c937d461a0 Mon Sep 17 00:00:00 2001 From: z4yx Date: Mon, 27 Nov 2023 18:49:08 +0800 Subject: [PATCH 4/4] bump version to 1.2.0 --- Cargo.lock | 2 +- Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 7a23ba7..f9db504 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -177,7 +177,7 @@ dependencies = [ [[package]] name = "pam_rssh" -version = "1.1.0" +version = "1.2.0" dependencies = [ "base64", "byteorder", diff --git a/Cargo.toml b/Cargo.toml index 41143c7..7713e30 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pam_rssh" -version = "1.1.0" +version = "1.2.0" authors = ["Yuxiang Zhang"] edition = "2018"