Signed-off-by: eternal-flame-AD <yume@yumechi.jp>
This commit is contained in:
ゆめ 2024-11-07 15:19:54 -06:00
commit 54b1bc412a
No known key found for this signature in database
7 changed files with 1891 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!");
}

273
src/lib.rs Normal file
View file

@ -0,0 +1,273 @@
#![warn(clippy::pedantic)]
#![allow(clippy::missing_errors_doc)]
use std::ffi::{c_char, c_int, CStr};
use std::io::Write;
use pam_sys::{pam_get_authtok, pam_get_user, 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, Debug)]
pub struct VaultResponse<D> {
data: D,
}
#[derive(Deserialize, Debug)]
pub struct KVData {
data: Option<KVDataData>,
}
#[derive(Deserialize, Debug)]
pub struct KVDataData {
password: Option<String>,
}
pub fn renew_token(env: &Environment) -> Result<bool, reqwest::Error> {
let client = reqwest::blocking::Client::new();
let url = format!("{}/v1/auth/token/renew-self", env.vault_url);
let res = client
.post(&url)
.header("X-Vault-Token", env.vault_token.as_str())
.header("Content-Type", "application/json")
.body(r#"{"increment": "3d"}"#)
.send()?
.error_for_status()?;
Ok(res.status().is_success())
}
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,
})
}
}
macro_rules! maybe_write {
($f:expr, $fmt:expr $(, $arg:tt)*) => {
if let Some(f) = $f.as_mut() {
writeln!(f, $fmt $(,$arg)*).expect("Could not write to debug file");
}
};
}
fn check_vault(
log: &mut Option<impl Write>,
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
);
maybe_write!(log, "url: {}", url);
if let Err(e) = renew_token(env) {
maybe_write!(log, "Error renewing token: {:?}", e);
}
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);
}
maybe_write!(log, "Password not found in response");
}
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 mut vault_conf = "/etc/vault.conf";
let mut debug = false;
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 == "debug" {
debug = true;
}
if arg.starts_with("vault_conf=") {
vault_conf = arg.split_once('=').unwrap().1;
}
}
let mut debug_out = if debug {
Some(
std::fs::OpenOptions::new()
.append(true)
.create(true)
.open("/tmp/pam_vault_debug.log")
.expect("Could not open debug file"),
)
} else {
None
};
let env = check_result!(Environment::from_env_file(vault_conf));
let mut user = std::ptr::null();
if PAM_SUCCESS != pam_get_user(pamh, &mut user, std::ptr::null()) {
return PAM_AUTH_ERR;
};
let mut token = std::ptr::null();
if PAM_SUCCESS != pam_get_authtok(pamh, pam_sys::PAM_AUTHTOK, &mut token, std::ptr::null()) {
return PAM_AUTH_ERR;
};
let (user, token) = (
check_result!(CStr::from_ptr(user as *const c_char).to_str()),
check_result!(CStr::from_ptr(token as *const c_char).to_str()),
);
let (token_key, token_value) = token.split_once('=').unwrap_or_else(|| ("default", &token));
if user.is_empty()
|| token_value.len() < 8
|| !valid_token_key(token_key)
|| !valid_user(&user)
|| !valid_token(&token)
{
return PAM_AUTH_ERR;
}
maybe_write!(debug_out, "token_key: {}", token_key);
match check_vault(&mut debug_out, &env, &user, token_key, token_value) {
Ok(true) => PAM_SUCCESS,
Ok(false) => PAM_AUTH_ERR,
Err(e) => {
eprintln!("Error: {e:?}");
PAM_ABORT
}
}
}
#[no_mangle]
pub unsafe extern "C" fn pam_sm_acct_mgmt(
_pamh: *mut pam_handle_t,
_flags: c_int,
_arg_count: c_int,
_arg_value: *const *const c_char,
) -> c_int {
PAM_SUCCESS
}
#[no_mangle]
pub unsafe extern "C" fn pam_sm_setcred(
_pamh: *mut pam_handle_t,
_flags: c_int,
_arg_count: c_int,
_arg_value: *const *const c_char,
) -> c_int {
PAM_SUCCESS
}

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"));

2
wrapper.h Normal file
View file

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