From 81c11d3f3040b3ac419db4b39ee8301d7d30ed03 Mon Sep 17 00:00:00 2001 From: Hatter Jiang Date: Sat, 26 Jul 2025 10:10:52 +0800 Subject: [PATCH] feat: clone from: https://github.com/anowell/pam-rs --- Cargo.toml | 2 + LICENSE | 22 ++++ README.md | 25 +++- pam-http/Cargo.toml | 12 ++ pam-http/Justfile | 12 ++ pam-http/README.md | 35 ++++++ pam-http/conf/http-auth | 2 + pam-http/src/lib.rs | 88 +++++++++++++ pam-http/test.c | 52 ++++++++ pam-sober/Cargo.toml | 12 ++ pam-sober/Justfile | 12 ++ pam-sober/conf/sober-auth | 2 + pam-sober/src/lib.rs | 73 +++++++++++ pam-sober/test.c | 53 ++++++++ pam/Cargo.toml | 16 +++ pam/src/constants.rs | 66 ++++++++++ pam/src/conv.rs | 94 ++++++++++++++ pam/src/items.rs | 88 +++++++++++++ pam/src/lib.rs | 34 +++++ pam/src/macros.rs | 141 +++++++++++++++++++++ pam/src/module.rs | 255 ++++++++++++++++++++++++++++++++++++++ 21 files changed, 1095 insertions(+), 1 deletion(-) create mode 100644 Cargo.toml create mode 100644 LICENSE create mode 100644 pam-http/Cargo.toml create mode 100644 pam-http/Justfile create mode 100644 pam-http/README.md create mode 100644 pam-http/conf/http-auth create mode 100644 pam-http/src/lib.rs create mode 100644 pam-http/test.c create mode 100644 pam-sober/Cargo.toml create mode 100644 pam-sober/Justfile create mode 100644 pam-sober/conf/sober-auth create mode 100644 pam-sober/src/lib.rs create mode 100644 pam-sober/test.c create mode 100644 pam/Cargo.toml create mode 100644 pam/src/constants.rs create mode 100644 pam/src/conv.rs create mode 100644 pam/src/items.rs create mode 100755 pam/src/lib.rs create mode 100644 pam/src/macros.rs create mode 100755 pam/src/module.rs diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..4a3224d --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,2 @@ +[workspace] +members = ["pam", "pam-sober", "pam-http"] diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..d206a5e --- /dev/null +++ b/LICENSE @@ -0,0 +1,22 @@ +The MIT License (MIT) + +Copyright (c) 2015 TOZNY + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + diff --git a/README.md b/README.md index 8521b60..bedec4f 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,25 @@ -# pam-rs +pam-rs +======== +Rust interface to the pluggable authentication module framework (PAM). + +The goal of this library is to provide a type-safe API that can be used to +interact with PAM. The library is incomplete - currently it supports a subset +of functions for use in a pam authentication module. A pam module is a shared +library that is invoked to authenticate a user, or to perform other functions. + +## 🌐 [pam-http](pam-http) + +An example of using pam-rs by performing HTTP basic access auth to authenticate users. + +## 🍻 [pam-sober](pam-sober) + +If you aren't sober enough for basic math, you can't login! + +### Credits + +The contents of this repo are heavily borrowed from: + +- [tozny/rust-pam](https://github.com/tozny/rust-pam) +- [ndenev/pam_groupmap](https://github.com/ndenev/pam_groupmap) +- [beatgammit/pam-http](https://github.com/beatgammit/pam-http) diff --git a/pam-http/Cargo.toml b/pam-http/Cargo.toml new file mode 100644 index 0000000..dabab3a --- /dev/null +++ b/pam-http/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "pam-http" +version = "0.1.0" +authors = ["Anthony Nowell "] + +[lib] +name = "pam_http" +crate-type = ["cdylib"] + +[dependencies] +pam-bindings = { path = "../pam/" } +reqwest = { version = "0.11.3", features = ["blocking"] } diff --git a/pam-http/Justfile b/pam-http/Justfile new file mode 100644 index 0000000..5769247 --- /dev/null +++ b/pam-http/Justfile @@ -0,0 +1,12 @@ + +all: + cargo build + +install: + @cargo build --release + sudo cp conf/http-auth /etc/pam.d/ + sudo cp ../target/release/libpam_http.so /lib/security/pam_http.so + +test: + @just install + gcc -o ../target/pam_test test.c -lpam -lpam_misc diff --git a/pam-http/README.md b/pam-http/README.md new file mode 100644 index 0000000..84276c3 --- /dev/null +++ b/pam-http/README.md @@ -0,0 +1,35 @@ +pam-http +======== + +A PAM HTTP BasicAuth module built using pam-rs + +# Prerequisites + +You need some libraries before you build like libpam and libssl. + +If you're going to build on Ubuntu, just run this: + +``` +sudo apt-get install -y build-essential libpam0g-dev libpam0g libssl-dev +``` + +# Building + +Just use `cargo build`. + +# Usage + +You need to move the build product to a folder where PAM is looking for modules. + +If you're using Ubuntu you can move `libpam_http.so` to `/lib/security`. +After doing so you need to make sure it has proper permissions: `sudo chmod 755 /lib/security/libpam_http.so`. +Then you can place a configuration file in `/etc/pam.d/`. It can look something like this: + +``` +auth sufficient libpam_http.so url=https://theserver.example.com/someendpoint +account sufficient libpam_http.so +``` + +Make sure the endpoint you're specifying can receive GET requests and supports +[HTTP Basic Authentication](https://en.wikipedia.org/wiki/Basic_access_authentication#Client_side). +If the user is authenticated successfully it should return HTTP 200. diff --git a/pam-http/conf/http-auth b/pam-http/conf/http-auth new file mode 100644 index 0000000..f866fcf --- /dev/null +++ b/pam-http/conf/http-auth @@ -0,0 +1,2 @@ +auth sufficient pam_http.so url=http://localhost:3000 +account sufficient pam_http.so diff --git a/pam-http/src/lib.rs b/pam-http/src/lib.rs new file mode 100644 index 0000000..48da6fd --- /dev/null +++ b/pam-http/src/lib.rs @@ -0,0 +1,88 @@ +extern crate pam; +extern crate reqwest; + +use pam::constants::{PamFlag, PamResultCode, PAM_PROMPT_ECHO_OFF}; +use pam::conv::Conv; +use pam::module::{PamHandle, PamHooks}; +use reqwest::blocking::Client; +use reqwest::StatusCode; +use std::collections::HashMap; +use std::ffi::CStr; +use std::time::Duration; +use pam::pam_try; + +struct PamHttp; +pam::pam_hooks!(PamHttp); + +impl PamHooks for PamHttp { + // This function performs the task of authenticating the user. + fn sm_authenticate(pamh: &mut PamHandle, args: Vec<&CStr>, _flags: PamFlag) -> PamResultCode { + println!("Let's auth over HTTP"); + + let args: Vec<_> = args + .iter() + .map(|s| s.to_string_lossy()) + .collect(); + let args: HashMap<&str, &str> = args + .iter() + .map(|s| { + let mut parts = s.splitn(2, '='); + (parts.next().unwrap(), parts.next().unwrap_or("")) + }) + .collect(); + + let user = pam_try!(pamh.get_user(None)); + + let url: &str = match args.get("url") { + Some(url) => url, + None => return PamResultCode::PAM_AUTH_ERR, + }; + + let conv = match pamh.get_item::() { + Ok(Some(conv)) => conv, + Ok(None) => { + unreachable!("No conv available"); + } + Err(err) => { + println!("Couldn't get pam_conv"); + return err; + } + }; + let password = pam_try!(conv.send(PAM_PROMPT_ECHO_OFF, "Word, yo: ")); + let password = match password { + Some(password) => Some(pam_try!(password.to_str(), PamResultCode::PAM_AUTH_ERR)), + None => None, + }; + println!("Got a password {:?}", password); + let status = pam_try!( + get_url(url, &user, password), + PamResultCode::PAM_AUTH_ERR + ); + + if !status.is_success() { + println!("HTTP Error: {}", status); + return PamResultCode::PAM_AUTH_ERR; + } + + PamResultCode::PAM_SUCCESS + } + + fn sm_setcred(_pamh: &mut PamHandle, _args: Vec<&CStr>, _flags: PamFlag) -> PamResultCode { + println!("set credentials"); + PamResultCode::PAM_SUCCESS + } + + fn acct_mgmt(_pamh: &mut PamHandle, _args: Vec<&CStr>, _flags: PamFlag) -> PamResultCode { + println!("account management"); + PamResultCode::PAM_SUCCESS + } +} + +fn get_url(url: &str, user: &str, password: Option<&str>) -> reqwest::Result { + let client = Client::builder().timeout(Duration::from_secs(15)).build()?; + client + .get(url) + .basic_auth(user, password) + .send() + .map(|r| r.status()) +} diff --git a/pam-http/test.c b/pam-http/test.c new file mode 100644 index 0000000..3cb9d49 --- /dev/null +++ b/pam-http/test.c @@ -0,0 +1,52 @@ +#include +#include +#include + +const struct pam_conv conv = { + misc_conv, + NULL +}; + +int main(int argc, char *argv[]) { + pam_handle_t* pamh = NULL; + int retval; + const char* user = "nobody"; + + if(argc != 2) { + printf("Usage: app [username]\n"); + exit(1); + } + + user = argv[1]; + + retval = pam_start("http-auth", user, &conv, &pamh); + + // Are the credentials correct? + if (retval == PAM_SUCCESS) { + printf("Credentials accepted.\n"); + retval = pam_authenticate(pamh, 0); + } + + // Can the accound be used at this time? + if (retval == PAM_SUCCESS) { + printf("Account is valid.\n"); + retval = pam_acct_mgmt(pamh, 0); + } + + // Did everything work? + if (retval == PAM_SUCCESS) { + printf("Authenticated\n"); + } else { + printf("Not Authenticated\n"); + } + + // close PAM (end session) + if (pam_end(pamh, retval) != PAM_SUCCESS) { + pamh = NULL; + printf("check_user: failed to release authenticator\n"); + exit(1); + } + + return retval == PAM_SUCCESS ? 0 : 1; +} + diff --git a/pam-sober/Cargo.toml b/pam-sober/Cargo.toml new file mode 100644 index 0000000..ffe12db --- /dev/null +++ b/pam-sober/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "pam-sober" +version = "0.1.0" +authors = ["Anthony Nowell "] + +[lib] +name = "pam_sober" +crate-type = ["cdylib"] + +[dependencies] +pam-bindings = { path = "../pam/" } +rand = "0.8.4" diff --git a/pam-sober/Justfile b/pam-sober/Justfile new file mode 100644 index 0000000..29a5b5a --- /dev/null +++ b/pam-sober/Justfile @@ -0,0 +1,12 @@ + +all: + cargo build + +install: + @cargo build --release + sudo cp conf/sober-auth /etc/pam.d/ + sudo cp ../target/release/libpam_sober.so /lib/security/pam_sober.so + +test: + @just install + gcc -o ../target/pam_test test.c -lpam -lpam_misc diff --git a/pam-sober/conf/sober-auth b/pam-sober/conf/sober-auth new file mode 100644 index 0000000..015f309 --- /dev/null +++ b/pam-sober/conf/sober-auth @@ -0,0 +1,2 @@ +auth sufficient pam_sober.so +account sufficient pam_sober.so diff --git a/pam-sober/src/lib.rs b/pam-sober/src/lib.rs new file mode 100644 index 0000000..d1c9b30 --- /dev/null +++ b/pam-sober/src/lib.rs @@ -0,0 +1,73 @@ +extern crate pam; +extern crate rand; + +use pam::constants::{PamFlag, PamResultCode, PAM_PROMPT_ECHO_ON}; +use pam::conv::Conv; +use pam::module::{PamHandle, PamHooks}; +use rand::Rng; +use std::ffi::CStr; +use std::str::FromStr; +use pam::pam_try; + +struct PamSober; +pam::pam_hooks!(PamSober); + +impl PamHooks for PamSober { + // This function performs the task of authenticating the user. + fn sm_authenticate(pamh: &mut PamHandle, _args: Vec<&CStr>, _flags: PamFlag) -> PamResultCode { + println!("Let's make sure you're sober enough to perform basic addition"); + + /* TODO: use args to change difficulty ;-) + let args: HashMap<&str, &str> = args.iter().map(|s| { + let mut parts = s.splitn(2, "="); + (parts.next().unwrap(), parts.next().unwrap_or("")) + }).collect(); + */ + + // TODO: maybe we can change difficulty base on user? + // let user = pam_try!(pam.get_user(None)); + + let conv = match pamh.get_item::() { + Ok(Some(conv)) => conv, + Ok(None) => todo!(), + Err(err) => { + println!("Couldn't get pam_conv"); + return err; + } + }; + + let mut rng = rand::thread_rng(); + let a = rng.gen::() % 100; + let b = rng.gen::() % 100; + let math = format!("{} + {} = ", a, b); + + // This println kinda helps debugging since the test script doesn't echo + eprintln!("[DEBUG]: {}{}", math, a + b); + + let password = pam_try!(conv.send(PAM_PROMPT_ECHO_ON, &math)); + + if let Some(password) = password { + let password = pam_try!(password.to_str(), PamResultCode::PAM_AUTH_ERR); + let answer = pam_try!(u32::from_str(password), PamResultCode::PAM_AUTH_ERR); + if answer == a + b { + PamResultCode::PAM_SUCCESS + } else { + println!("Wrong answer provided {} + {} != {}", a, b, answer); + PamResultCode::PAM_AUTH_ERR + } + } else { + println!("You failed the PAM sobriety test."); + PamResultCode::PAM_AUTH_ERR + } + } + + fn sm_setcred(_pamh: &mut PamHandle, _args: Vec<&CStr>, _flags: PamFlag) -> PamResultCode { + println!("set credentials"); + PamResultCode::PAM_SUCCESS + } + + fn acct_mgmt(_pamh: &mut PamHandle, _args: Vec<&CStr>, _flags: PamFlag) -> PamResultCode { + println!("account management"); + PamResultCode::PAM_SUCCESS + } +} diff --git a/pam-sober/test.c b/pam-sober/test.c new file mode 100644 index 0000000..e55d7d1 --- /dev/null +++ b/pam-sober/test.c @@ -0,0 +1,53 @@ +#include +#include +#include + +const struct pam_conv conv = { + misc_conv, + NULL +}; + +int main(int argc, char *argv[]) { + pam_handle_t* pamh = NULL; + int retval; + const char* user = "nobody"; + + if(argc != 2) { + printf("Usage: app [username]\n"); + exit(1); + } + + user = argv[1]; + + retval = pam_start("sober-auth", user, &conv, &pamh); + + // Are the credentials correct? + if (retval == PAM_SUCCESS) { + printf("PAM module initialized\n"); + retval = pam_authenticate(pamh, 0); + } + + // Can the accound be used at this time? + if (retval == PAM_SUCCESS) { + printf("Credentials accepted.\n"); + retval = pam_acct_mgmt(pamh, 0); + } + + // Did everything work? + if (retval == PAM_SUCCESS) { + printf("Account is valid.\n"); + printf("Authenticated\n"); + } else { + printf("Not Authenticated\n"); + } + + // close PAM (end session) + if (pam_end(pamh, retval) != PAM_SUCCESS) { + pamh = NULL; + printf("check_user: failed to release authenticator\n"); + exit(1); + } + + return retval == PAM_SUCCESS ? 0 : 1; +} + diff --git a/pam/Cargo.toml b/pam/Cargo.toml new file mode 100644 index 0000000..1dc4f3b --- /dev/null +++ b/pam/Cargo.toml @@ -0,0 +1,16 @@ +[package] + +name = "pam-bindings" +description = "PAM bindings for Rust" +version = "0.1.1" +authors = [ "Anthony Nowell " ] +repository = "https://github.com/anowell/pam-rs" +readme = "../README.md" +keywords = ["pam", "ffi", "linux", "authentication"] +license = "MIT" + +[lib] +name = "pam" + +[dependencies] +libc = "0.2.97" diff --git a/pam/src/constants.rs b/pam/src/constants.rs new file mode 100644 index 0000000..ba768c8 --- /dev/null +++ b/pam/src/constants.rs @@ -0,0 +1,66 @@ +use libc::{c_int, c_uint}; + +// TODO: Import constants from C header file at compile time. + +pub type PamFlag = c_uint; +pub type PamItemType = c_int; +pub type PamMessageStyle = c_int; + +// The Linux-PAM flags +// see /usr/include/security/_pam_types.h +pub const PAM_SILENT: PamFlag = 0x8000; +pub const PAM_DISALLOW_NULL_AUTHTOK: PamFlag = 0x0001; +pub const PAM_ESTABLISH_CRED: PamFlag = 0x0002; +pub const PAM_DELETE_CRED: PamFlag = 0x0004; +pub const PAM_REINITIALIZE_CRED: PamFlag = 0x0008; +pub const PAM_REFRESH_CRED: PamFlag = 0x0010; +pub const PAM_CHANGE_EXPIRED_AUTHTOK: PamFlag = 0x0020; + +// Message styles +pub const PAM_PROMPT_ECHO_OFF: PamMessageStyle = 1; +pub const PAM_PROMPT_ECHO_ON: PamMessageStyle = 2; +pub const PAM_ERROR_MSG: PamMessageStyle = 3; +pub const PAM_TEXT_INFO: PamMessageStyle = 4; +/// yes/no/maybe conditionals +pub const PAM_RADIO_TYPE: PamMessageStyle = 5; +pub const PAM_BINARY_PROMPT: PamMessageStyle = 7; + +// The Linux-PAM return values +// see /usr/include/security/_pam_types.h +#[allow(non_camel_case_types, dead_code)] +#[derive(Debug, PartialEq)] +#[repr(C)] +pub enum PamResultCode { + PAM_SUCCESS = 0, + PAM_OPEN_ERR = 1, + PAM_SYMBOL_ERR = 2, + PAM_SERVICE_ERR = 3, + PAM_SYSTEM_ERR = 4, + PAM_BUF_ERR = 5, + PAM_PERM_DENIED = 6, + PAM_AUTH_ERR = 7, + PAM_CRED_INSUFFICIENT = 8, + PAM_AUTHINFO_UNAVAIL = 9, + PAM_USER_UNKNOWN = 10, + PAM_MAXTRIES = 11, + PAM_NEW_AUTHTOK_REQD = 12, + PAM_ACCT_EXPIRED = 13, + PAM_SESSION_ERR = 14, + PAM_CRED_UNAVAIL = 15, + PAM_CRED_EXPIRED = 16, + PAM_CRED_ERR = 17, + PAM_NO_MODULE_DATA = 18, + PAM_CONV_ERR = 19, + PAM_AUTHTOK_ERR = 20, + PAM_AUTHTOK_RECOVERY_ERR = 21, + PAM_AUTHTOK_LOCK_BUSY = 22, + PAM_AUTHTOK_DISABLE_AGING = 23, + PAM_TRY_AGAIN = 24, + PAM_IGNORE = 25, + PAM_ABORT = 26, + PAM_AUTHTOK_EXPIRED = 27, + PAM_MODULE_UNKNOWN = 28, + PAM_BAD_ITEM = 29, + PAM_CONV_AGAIN = 30, + PAM_INCOMPLETE = 31, +} diff --git a/pam/src/conv.rs b/pam/src/conv.rs new file mode 100644 index 0000000..ff3b4d4 --- /dev/null +++ b/pam/src/conv.rs @@ -0,0 +1,94 @@ +use libc::{c_char, c_int}; +use std::ffi::{CStr, CString}; +use std::ptr; + +use constants::PamResultCode; +use constants::PamMessageStyle; +use items::Item; +use module::PamResult; + +#[repr(C)] +struct PamMessage { + msg_style: PamMessageStyle, + msg: *const c_char, +} + +#[repr(C)] +struct PamResponse { + resp: *const c_char, + resp_retcode: libc::c_int, // Unused - always zero +} + +/// `PamConv` acts as a channel for communicating with user. +/// +/// Communication is mediated by the pam client (the application that invoked +/// pam). Messages sent will be relayed to the user by the client, and response +/// will be relayed back. +#[repr(C)] +pub struct Inner { + conv: extern "C" fn( + num_msg: c_int, + pam_message: &&PamMessage, + pam_response: &mut *const PamResponse, + appdata_ptr: *const libc::c_void, + ) -> PamResultCode, + appdata_ptr: *const libc::c_void, +} + +pub struct Conv<'a>(&'a Inner); + +impl<'a> Conv<'a> { + /// Sends a message to the pam client. + /// + /// This will typically result in the user seeing a message or a prompt. + /// There are several message styles available: + /// + /// - PAM_PROMPT_ECHO_OFF + /// - PAM_PROMPT_ECHO_ON + /// - PAM_ERROR_MSG + /// - PAM_TEXT_INFO + /// - PAM_RADIO_TYPE + /// - PAM_BINARY_PROMPT + /// + /// Note that the user experience will depend on how the client implements + /// these message styles - and not all applications implement all message + /// styles. + pub fn send(&self, style: PamMessageStyle, msg: &str) -> PamResult> { + let mut resp_ptr: *const PamResponse = ptr::null(); + let msg_cstr = CString::new(msg).unwrap(); + let msg = PamMessage { + msg_style: style, + msg: msg_cstr.as_ptr(), + }; + + let ret = (self.0.conv)(1, &&msg, &mut resp_ptr, self.0.appdata_ptr); + + if PamResultCode::PAM_SUCCESS == ret { + // PamResponse.resp is null for styles that don't return user input like PAM_TEXT_INFO + let response = unsafe { (*resp_ptr).resp }; + if response.is_null() { + Ok(None) + } else { + Ok(Some(unsafe { CStr::from_ptr(response) })) + } + } else { + Err(ret) + } + } +} + +impl<'a> Item for Conv<'a> { + type Raw = Inner; + + fn type_id() -> crate::items::ItemType { + crate::items::ItemType::Conv + } + + unsafe fn from_raw(raw: *const Self::Raw) -> Self { + Self(&*raw) + } + + fn into_raw(self) -> *const Self::Raw { + self.0 as _ + } +} diff --git a/pam/src/items.rs b/pam/src/items.rs new file mode 100644 index 0000000..a1e18ac --- /dev/null +++ b/pam/src/items.rs @@ -0,0 +1,88 @@ +#[repr(u32)] +pub enum ItemType { + /// The service name + Service = 1, + /// The user name + User = 2, + /// The tty name + Tty = 3, + /// The remote host name + RHost = 4, + /// The pam_conv structure + Conv = 5, + /// The authentication token (password) + AuthTok = 6, + /// The old authentication token + OldAuthTok = 7, + /// The remote user name + RUser = 8, + /// the prompt for getting a username + UserPrompt = 9, + /// app supplied function to override failure delays + FailDelay = 10, + /// X :display name + XDisplay = 11, + /// X :server authentication data + XAuthData = 12, + /// The type for pam_get_authtok + AuthTokType = 13, +} + +// A type that can be requested by `pam::Handle::get_item`. +pub trait Item { + /// The `repr(C)` type that is returned (by pointer) by the underlying `pam_get_item` function. + type Raw; + + /// The `ItemType` for this type + fn type_id() -> ItemType; + + /// The function to convert from the pointer to the C-representation to this safer wrapper type + /// + /// # Safety + /// + /// This function can assume the pointer is a valid pointer to a `Self::Raw` instance. + unsafe fn from_raw(raw: *const Self::Raw) -> Self; + + /// The function to convert from this wrapper type to a C-compatible pointer. + fn into_raw(self) -> *const Self::Raw; +} + +macro_rules! cstr_item { + ($name:ident) => { + #[derive(Debug)] + pub struct $name<'s>(pub &'s std::ffi::CStr); + + impl<'s> std::ops::Deref for $name<'s> { + type Target = &'s std::ffi::CStr; + fn deref(&self) -> &Self::Target { + &self.0 + } + } + + impl<'s> Item for $name<'s> { + type Raw = libc::c_char; + + fn type_id() -> ItemType { + ItemType::$name + } + + unsafe fn from_raw(raw: *const Self::Raw) -> Self { + Self(std::ffi::CStr::from_ptr(raw)) + } + + fn into_raw(self) -> *const Self::Raw { + self.0.as_ptr() + } + } + }; +} + +cstr_item!(Service); +cstr_item!(User); +cstr_item!(Tty); +cstr_item!(RHost); +// Conv +cstr_item!(AuthTok); +cstr_item!(OldAuthTok); +cstr_item!(RUser); +cstr_item!(UserPrompt); diff --git a/pam/src/lib.rs b/pam/src/lib.rs new file mode 100755 index 0000000..d146b5b --- /dev/null +++ b/pam/src/lib.rs @@ -0,0 +1,34 @@ +//! Interface to the pluggable authentication module framework (PAM). +//! +//! The goal of this library is to provide a type-safe API that can be used to +//! interact with PAM. The library is incomplete - currently it supports +//! a subset of functions for use in a pam authentication module. A pam module +//! is a shared library that is invoked to authenticate a user, or to perform +//! other functions. +//! +//! For general information on writing pam modules, see +//! [The Linux-PAM Module Writers' Guide][module-guide] +//! +//! [module-guide]: http://www.linux-pam.org/Linux-PAM-html/Linux-PAM_MWG.html +//! +//! A typical authentication module will define an external function called +//! `pam_sm_authenticate()`, which will use functions in this library to +//! interrogate the program that requested authentication for more information, +//! and to render a result. For a working example that uses this library, see +//! [toznyauth-pam][]. +//! +//! [toznyauth-pam]: https://github.com/tozny/toznyauth-pam +//! +//! Note that constants that are normally read from pam header files are +//! hard-coded in the `constants` module. The values there are taken from +//! a Linux system. That means that it might take some work to get this library +//! to work on other platforms. + +extern crate libc; + +pub mod constants; +pub mod conv; +pub mod items; +#[doc(hidden)] +pub mod macros; +pub mod module; diff --git a/pam/src/macros.rs b/pam/src/macros.rs new file mode 100644 index 0000000..5cf706a --- /dev/null +++ b/pam/src/macros.rs @@ -0,0 +1,141 @@ +/// Macro to generate the `extern "C"` entrypoint bindings needed by PAM +/// +/// You can call `pam_hooks!(SomeType);` for any type that implements `PamHooks` +/// +/// ## Examples: +/// +/// Here is full example of a PAM module that would authenticate and authorize everybody: +/// +/// ``` +/// #[macro_use] extern crate pam; +/// +/// use pam::module::{PamHooks, PamHandle}; +/// use pam::constants::{PamResultCode, PamFlag}; +/// use std::ffi::CStr; +/// +/// # fn main() {} +/// struct MyPamModule; +/// pam_hooks!(MyPamModule); +/// +/// impl PamHooks for MyPamModule { +/// fn sm_authenticate(pamh: &mut PamHandle, args: Vec<&CStr>, flags: PamFlag) -> PamResultCode { +/// println!("Everybody is authenticated!"); +/// PamResultCode::PAM_SUCCESS +/// } +/// +/// fn acct_mgmt(pamh: &mut PamHandle, args: Vec<&CStr>, flags: PamFlag) -> PamResultCode { +/// println!("Everybody is authorized!"); +/// PamResultCode::PAM_SUCCESS +/// } +/// } +/// ``` +#[macro_export] +macro_rules! pam_hooks { + ($ident:ident) => { + pub use self::pam_hooks_scope::*; + mod pam_hooks_scope { + use std::ffi::CStr; + use std::os::raw::{c_char, c_int}; + use $crate::constants::{PamFlag, PamResultCode}; + use $crate::module::{PamHandle, PamHooks}; + + fn extract_argv<'a>(argc: c_int, argv: *const *const c_char) -> Vec<&'a CStr> { + (0..argc) + .map(|o| unsafe { CStr::from_ptr(*argv.offset(o as isize) as *const c_char) }) + .collect() + } + + #[no_mangle] + pub extern "C" fn pam_sm_acct_mgmt( + pamh: &mut PamHandle, + flags: PamFlag, + argc: c_int, + argv: *const *const c_char, + ) -> PamResultCode { + let args = extract_argv(argc, argv); + super::$ident::acct_mgmt(pamh, args, flags) + } + + #[no_mangle] + pub extern "C" fn pam_sm_authenticate( + pamh: &mut PamHandle, + flags: PamFlag, + argc: c_int, + argv: *const *const c_char, + ) -> PamResultCode { + let args = extract_argv(argc, argv); + super::$ident::sm_authenticate(pamh, args, flags) + } + + #[no_mangle] + pub extern "C" fn pam_sm_chauthtok( + pamh: &mut PamHandle, + flags: PamFlag, + argc: c_int, + argv: *const *const c_char, + ) -> PamResultCode { + let args = extract_argv(argc, argv); + super::$ident::sm_chauthtok(pamh, args, flags) + } + + #[no_mangle] + pub extern "C" fn pam_sm_close_session( + pamh: &mut PamHandle, + flags: PamFlag, + argc: c_int, + argv: *const *const c_char, + ) -> PamResultCode { + let args = extract_argv(argc, argv); + super::$ident::sm_close_session(pamh, args, flags) + } + + #[no_mangle] + pub extern "C" fn pam_sm_open_session( + pamh: &mut PamHandle, + flags: PamFlag, + argc: c_int, + argv: *const *const c_char, + ) -> PamResultCode { + let args = extract_argv(argc, argv); + super::$ident::sm_open_session(pamh, args, flags) + } + + #[no_mangle] + pub extern "C" fn pam_sm_setcred( + pamh: &mut PamHandle, + flags: PamFlag, + argc: c_int, + argv: *const *const c_char, + ) -> PamResultCode { + let args = extract_argv(argc, argv); + super::$ident::sm_setcred(pamh, args, flags) + } + } + }; +} + +#[macro_export] +macro_rules! pam_try { + ($r:expr) => { + match $r { + Ok(t) => t, + Err(e) => return e, + } + }; + ($r:expr, $e:expr) => { + match $r { + Ok(t) => t, + Err(_) => return $e, + } + }; +} + +#[cfg(test)] +pub mod test { + use module::PamHooks; + + struct Foo; + impl PamHooks for Foo {} + + pam_hooks!(Foo); +} diff --git a/pam/src/module.rs b/pam/src/module.rs new file mode 100755 index 0000000..0e76215 --- /dev/null +++ b/pam/src/module.rs @@ -0,0 +1,255 @@ +//! Functions for use in pam modules. + +use libc::c_char; +use std::ffi::{CStr, CString}; + +use constants::{PamFlag, PamResultCode}; + +/// Opaque type, used as a pointer when making pam API calls. +/// +/// A module is invoked via an external function such as `pam_sm_authenticate`. +/// Such a call provides a pam handle pointer. The same pointer should be given +/// as an argument when making API calls. +#[repr(C)] +pub struct PamHandle { + _data: [u8; 0], +} + +#[link(name = "pam")] +extern "C" { + fn pam_get_data( + pamh: *const PamHandle, + module_data_name: *const c_char, + data: &mut *const libc::c_void, + ) -> PamResultCode; + + fn pam_set_data( + pamh: *const PamHandle, + module_data_name: *const c_char, + data: *mut libc::c_void, + cleanup: extern "C" fn( + pamh: *const PamHandle, + data: *mut libc::c_void, + error_status: PamResultCode, + ), + ) -> PamResultCode; + + fn pam_get_item( + pamh: *const PamHandle, + item_type: crate::items::ItemType, + item: &mut *const libc::c_void, + ) -> PamResultCode; + + fn pam_set_item( + pamh: *mut PamHandle, + item_type: crate::items::ItemType, + item: *const libc::c_void, + ) -> PamResultCode; + + fn pam_get_user( + pamh: *const PamHandle, + user: &*mut c_char, + prompt: *const c_char, + ) -> PamResultCode; +} + +pub extern "C" fn cleanup(_: *const PamHandle, c_data: *mut libc::c_void, _: PamResultCode) { + unsafe { + let _data: Box = Box::from_raw(c_data.cast::()); + } +} + +pub type PamResult = Result; + +impl PamHandle { + /// Gets some value, identified by `key`, that has been set by the module + /// previously. + /// + /// See `pam_get_data` in + /// http://www.linux-pam.org/Linux-PAM-html/mwg-expected-by-module-item.html + /// + /// # Errors + /// + /// Returns an error if the underlying PAM function call fails. + /// + /// # Safety + /// + /// The data stored under the provided key must be of type `T` otherwise the + /// behaviour of this funtion is undefined. + pub unsafe fn get_data<'a, T>(&'a self, key: &str) -> PamResult<&'a T> { + let c_key = CString::new(key).unwrap(); + let mut ptr: *const libc::c_void = std::ptr::null(); + let res = pam_get_data(self, c_key.as_ptr(), &mut ptr); + if PamResultCode::PAM_SUCCESS == res && !ptr.is_null() { + let typed_ptr = ptr.cast::(); + let data: &T = &*typed_ptr; + Ok(data) + } else { + Err(res) + } + } + + /// Stores a value that can be retrieved later with `get_data`. The value lives + /// as long as the current pam cycle. + /// + /// See `pam_set_data` in + /// http://www.linux-pam.org/Linux-PAM-html/mwg-expected-by-module-item.html + /// + /// # Errors + /// + /// Returns an error if the underlying PAM function call fails. + pub fn set_data(&self, key: &str, data: Box) -> PamResult<()> { + let c_key = CString::new(key).unwrap(); + let res = unsafe { + pam_set_data( + self, + c_key.as_ptr(), + Box::into_raw(data).cast::(), + cleanup::, + ) + }; + if PamResultCode::PAM_SUCCESS == res { + Ok(()) + } else { + Err(res) + } + } + + /// Retrieves a value that has been set, possibly by the pam client. This is + /// particularly useful for getting a `PamConv` reference. + /// + /// See `pam_get_item` in + /// http://www.linux-pam.org/Linux-PAM-html/mwg-expected-by-module-item.html + /// + /// # Errors + /// + /// Returns an error if the underlying PAM function call fails. + pub fn get_item(&self) -> PamResult> { + let mut ptr: *const libc::c_void = std::ptr::null(); + let (res, item) = unsafe { + let r = pam_get_item(self, T::type_id(), &mut ptr); + let typed_ptr = ptr.cast::(); + let t = if typed_ptr.is_null() { + None + } else { + Some(T::from_raw(typed_ptr)) + }; + (r, t) + }; + if PamResultCode::PAM_SUCCESS == res { + Ok(item) + } else { + Err(res) + } + } + + /// Sets a value in the pam context. The value can be retrieved using + /// `get_item`. + /// + /// Note that all items are strings, except `PAM_CONV` and `PAM_FAIL_DELAY`. + /// + /// See `pam_set_item` in + /// http://www.linux-pam.org/Linux-PAM-html/mwg-expected-by-module-item.html + /// + /// # Errors + /// + /// Returns an error if the underlying PAM function call fails. + /// + /// # Panics + /// + /// Panics if the provided item key contains a nul byte + pub fn set_item_str(&mut self, item: T) -> PamResult<()> { + let res = + unsafe { pam_set_item(self, T::type_id(), item.into_raw().cast::())}; + if PamResultCode::PAM_SUCCESS == res { + Ok(()) + } else { + Err(res) + } + } + + /// Retrieves the name of the user who is authenticating or logging in. + /// + /// This is really a specialization of `get_item`. + /// + /// See `pam_get_user` in + /// http://www.linux-pam.org/Linux-PAM-html/mwg-expected-by-module-item.html + /// + /// # Errors + /// + /// Returns an error if the underlying PAM function call fails. + /// + /// # Panics + /// + /// Panics if the provided prompt string contains a nul byte + pub fn get_user(&self, prompt: Option<&str>) -> PamResult { + let ptr: *mut c_char = std::ptr::null_mut(); + let prompt_string; + let c_prompt = match prompt { + Some(p) => { + prompt_string = CString::new(p).unwrap(); + prompt_string.as_ptr() + } + None => std::ptr::null(), + }; + let res = unsafe { pam_get_user(self, &ptr, c_prompt) }; + if PamResultCode::PAM_SUCCESS == res && !ptr.is_null() { + let const_ptr = ptr as *const c_char; + let bytes = unsafe { CStr::from_ptr(const_ptr).to_bytes() }; + String::from_utf8(bytes.to_vec()).map_err(|_| PamResultCode::PAM_CONV_ERR) + } else { + Err(res) + } + } +} + +/// Provides functions that are invoked by the entrypoints generated by the +/// [`pam_hooks!` macro](../macro.pam_hooks.html). +/// +/// All of hooks are ignored by PAM dispatch by default given the default return value of `PAM_IGNORE`. +/// Override any functions that you want to handle with your module. See `man pam(3)`. +#[allow(unused_variables)] +pub trait PamHooks { + /// This function performs the task of establishing whether the user is permitted to gain access at + /// this time. It should be understood that the user has previously been validated by an + /// authentication module. This function checks for other things. Such things might be: the time of + /// day or the date, the terminal line, remote hostname, etc. This function may also determine + /// things like the expiration on passwords, and respond that the user change it before continuing. + fn acct_mgmt(pamh: &mut PamHandle, args: Vec<&CStr>, flags: PamFlag) -> PamResultCode { + PamResultCode::PAM_IGNORE + } + + /// This function performs the task of authenticating the user. + fn sm_authenticate(pamh: &mut PamHandle, args: Vec<&CStr>, flags: PamFlag) -> PamResultCode { + PamResultCode::PAM_IGNORE + } + + /// This function is used to (re-)set the authentication token of the user. + /// + /// The PAM library calls this function twice in succession. The first time with + /// `PAM_PRELIM_CHECK` and then, if the module does not return `PAM_TRY_AGAIN`, subsequently with + /// `PAM_UPDATE_AUTHTOK`. It is only on the second call that the authorization token is + /// (possibly) changed. + fn sm_chauthtok(pamh: &mut PamHandle, args: Vec<&CStr>, flags: PamFlag) -> PamResultCode { + PamResultCode::PAM_IGNORE + } + + /// This function is called to terminate a session. + fn sm_close_session(pamh: &mut PamHandle, args: Vec<&CStr>, flags: PamFlag) -> PamResultCode { + PamResultCode::PAM_IGNORE + } + + /// This function is called to commence a session. + fn sm_open_session(pamh: &mut PamHandle, args: Vec<&CStr>, flags: PamFlag) -> PamResultCode { + PamResultCode::PAM_IGNORE + } + + /// This function performs the task of altering the credentials of the user with respect to the + /// corresponding authorization scheme. Generally, an authentication module may have access to more + /// information about a user than their authentication token. This function is used to make such + /// information available to the application. It should only be called after the user has been + /// authenticated but before a session has been established. + fn sm_setcred(pamh: &mut PamHandle, args: Vec<&CStr>, flags: PamFlag) -> PamResultCode { + PamResultCode::PAM_IGNORE + } +}