Signed-off-by: eternal-flame-AD <yume@yumechi.jp>
This commit is contained in:
ゆめ 2024-11-07 13:46:58 -06:00
commit 792d15fef5
No known key found for this signature in database
7 changed files with 1812 additions and 0 deletions

1
.gitignore vendored Normal file
View file

@ -0,0 +1 @@
/target

1560
Cargo.lock generated Normal file

File diff suppressed because it is too large Load diff

19
Cargo.toml Normal file
View file

@ -0,0 +1,19 @@
[package]
name = "pam-apppasswd-hashicorp-vault"
version = "0.1.0"
edition = "2021"
build = "build.rs"
[lib]
crate-type = ["cdylib", "rlib"]
[dependencies]
reqwest = { version = "0.12.9", default-features = false, features = ["charset", "json", "blocking", "rustls-tls", "rustls-tls-native-roots"] }
serde = { version = "1.0.214", features = ["derive"] }
[build-dependencies]
bindgen = "0.70.1"
[profile.release]
lto = true

31
build.rs Normal file
View file

@ -0,0 +1,31 @@
use std::env;
use std::path::PathBuf;
#[allow(deprecated, reason = "False positive")]
use bindgen::CargoCallbacks;
fn main() {
println!("cargo:rustc-link-lib=pam");
// The bindgen::Builder is the main entry point
// to bindgen, and lets you build up options for
// the resulting bindings.
let bindings = bindgen::Builder::default()
// The input header we would like to generate
// bindings for.
.header("wrapper.h")
.default_macro_constant_type(bindgen::MacroTypeVariation::Signed)
// Tell cargo to invalidate the built crate whenever any of the
// included header files changed.
.parse_callbacks(Box::new(CargoCallbacks::new()))
// Finish the builder and generate the bindings.
.generate()
// Unwrap the Result and panic on failure.
.expect("Unable to generate bindings");
// Write the bindings to the $OUT_DIR/bindings.rs file.
let out_path = PathBuf::from(env::var("OUT_DIR").unwrap());
bindings
.write_to_file(out_path.join("pam_sys.rs"))
.expect("Couldn't write bindings!");
}

195
src/lib.rs Normal file
View file

@ -0,0 +1,195 @@
#![warn(clippy::pedantic)]
#![allow(clippy::missing_errors_doc)]
use std::ffi::{c_char, c_int};
use pam_sys::{pam_handle_t, PAM_ABORT, PAM_AUTH_ERR, PAM_SUCCESS};
use serde::Deserialize;
pub mod pam_sys;
macro_rules! check_result {
($e:expr) => {{
let ret = $e;
match ret {
Ok(r) => r,
Err(ret) => {
eprintln!("Error: {:?}", ret);
return PAM_ABORT;
}
}
}};
}
pub struct Environment {
vault_url: String,
vault_token: String,
mount_point: String,
}
#[derive(Deserialize)]
pub struct VaultResponse<D> {
data: D,
}
#[derive(Deserialize)]
pub struct KVData {
data: Option<KVDataData>,
}
#[derive(Deserialize)]
pub struct KVDataData {
password: Option<String>,
}
impl Environment {
pub fn from_env() -> Result<Self, std::env::VarError> {
let vault_url = std::env::var("VAULT_ADDR")?;
let vault_token = std::env::var("VAULT_TOKEN")?;
let mount_point = std::env::var("VAULT_MOUNT_POINT")?;
Ok(Self {
vault_url,
vault_token,
mount_point,
})
}
pub fn from_env_file(file: &str) -> Result<Self, std::io::Error> {
let f = std::fs::read_to_string(file)?;
let mut vault_url = String::new();
let mut vault_token = String::new();
let mut mount_point = String::new();
for line in f.lines().filter(|l| !l.is_empty() && !l.starts_with('#')) {
let (key, value) = line.split_once('=').unwrap_or((line, ""));
match key {
"VAULT_ADDR" => vault_url = value.trim().to_string(),
"VAULT_TOKEN" => vault_token = value.trim().to_string(),
"VAULT_MOUNT_POINT" => mount_point = value.trim().to_string(),
_ => {}
}
}
Ok(Self {
vault_url,
vault_token,
mount_point,
})
}
}
fn check_vault(
env: &Environment,
user: &str,
key: &str,
token: &str,
) -> Result<bool, reqwest::Error> {
let client = reqwest::blocking::Client::new();
let url = format!(
"{}/v1/{}/data/{}/{}",
env.vault_url,
env.mount_point,
path_encode(user),
key
);
let res = client
.get(&url)
.header("X-Vault-Token", env.vault_token.as_str())
.send()?
.error_for_status()?
.json::<VaultResponse<KVData>>()?;
if let Some(data) = res.data.data {
if let Some(password) = data.password {
return Ok(password == token);
}
}
Ok(false)
}
fn path_encode(path: &str) -> String {
path.chars()
.map(|c| match c {
'a'..='z' | 'A'..='Z' | '0'..='9' | '-' | '_' => c.to_string(),
_ => format!("%{:02X}", c as u8),
})
.collect()
}
fn valid_user(user: &str) -> bool {
user.chars()
.all(|c| c.is_ascii_alphanumeric() || "_-.@".contains(c))
}
fn valid_token(token: &str) -> bool {
token
.chars()
.all(|c| c.is_ascii_alphanumeric() || "!@#$%^&*()_+-=".contains(c))
}
fn valid_token_key(token_key: &str) -> bool {
token_key
.chars()
.all(|c| c.is_ascii_alphanumeric() || "_".contains(c))
}
/// This function is called by PAM to authenticate the user.
///
/// # Safety
///
/// This function must be called by PAM with the correct arguments defined by the PAM API.
#[no_mangle]
pub unsafe extern "C" fn pam_sm_authenticate(
_pamh: *mut pam_handle_t,
_flags: c_int,
arg_count: c_int,
arg_value: *const *const c_char,
) -> c_int {
let user = check_result!(std::env::var("PAM_USER"));
let token = check_result!(std::env::var("PAM_AUTHTOK"));
let (token_key, token_value) = token.split_once('=').unwrap_or_else(|| ("default", &token));
if user.is_empty()
|| token_value.len() < 10
|| !valid_token_key(token_key)
|| !valid_user(&user)
|| !valid_token(&token)
{
return PAM_AUTH_ERR;
}
let mut vault_conf: *mut c_char = std::ptr::null_mut();
for i in 0..arg_count {
let arg = *arg_value.offset(i as isize);
let arg = check_result!(std::ffi::CStr::from_ptr(arg).to_str());
if arg.starts_with("vault_conf=") {
vault_conf = arg.trim_start_matches("vault_conf=").as_ptr() as *mut c_char;
}
}
if vault_conf.is_null() {
return PAM_AUTH_ERR;
}
let vault_conf = check_result!(std::ffi::CStr::from_ptr(vault_conf).to_str());
let env = check_result!(Environment::from_env_file(vault_conf));
match check_vault(&env, &user, token_key, token_value) {
Ok(true) => PAM_SUCCESS,
Ok(false) => PAM_AUTH_ERR,
Err(e) => {
eprintln!("Error: {e:?}");
PAM_ABORT
}
}
}

5
src/pam_sys.rs Normal file
View file

@ -0,0 +1,5 @@
#![allow(clippy::all)]
#![allow(clippy::pedantic)]
#![allow(non_camel_case_types)]
include!(concat!(env!("OUT_DIR"), "/pam_sys.rs"));

1
wrapper.h Normal file
View file

@ -0,0 +1 @@
#include <security/pam_modules.h>