From 1d3eaff62261b5e3dabcf1eb8f1970587675cbe1 Mon Sep 17 00:00:00 2001 From: Suyono Date: Mon, 16 Mar 2026 11:59:16 +1100 Subject: [PATCH] initial commit: WIP debugging PAM module --- .dockerignore | 2 + .gitignore | 5 + CMakeLists.txt | 16 +++ LICENSE | 200 ++++++++++++++++++++++++++ README.md | 82 +++++++++++ docker/Dockerfile | 21 +++ pam-module/CMakeLists.txt | 25 ++++ pam-module/include/rust_backend_ffi.h | 24 ++++ pam-module/src/auth_client.cpp | 26 ++++ pam-module/src/auth_client.h | 7 + pam-module/src/pam_module.cpp | 38 +++++ rust-backend/Cargo.lock | 16 +++ rust-backend/Cargo.toml | 16 +++ rust-backend/src/auth.rs | 14 ++ rust-backend/src/error.rs | 10 ++ rust-backend/src/lib.rs | 62 ++++++++ rust-backend/src/logging.rs | 32 +++++ tests/app/CMakeLists.txt | 7 + tests/app/pam_test_app.cpp | 95 ++++++++++++ tests/config/pam.d/mytestservice | 1 + 20 files changed, 699 insertions(+) create mode 100644 .dockerignore create mode 100644 .gitignore create mode 100644 CMakeLists.txt create mode 100644 LICENSE create mode 100644 README.md create mode 100644 docker/Dockerfile create mode 100644 pam-module/CMakeLists.txt create mode 100644 pam-module/include/rust_backend_ffi.h create mode 100644 pam-module/src/auth_client.cpp create mode 100644 pam-module/src/auth_client.h create mode 100644 pam-module/src/pam_module.cpp create mode 100644 rust-backend/Cargo.lock create mode 100644 rust-backend/Cargo.toml create mode 100644 rust-backend/src/auth.rs create mode 100644 rust-backend/src/error.rs create mode 100644 rust-backend/src/lib.rs create mode 100644 rust-backend/src/logging.rs create mode 100644 tests/app/CMakeLists.txt create mode 100644 tests/app/pam_test_app.cpp create mode 100644 tests/config/pam.d/mytestservice diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..05edfca --- /dev/null +++ b/.dockerignore @@ -0,0 +1,2 @@ +/docker +/build diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4b26dfc --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +build/ +rust-backend/target/ +pam-module/*.so +*.swp +*.log diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..798cceb --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,16 @@ +cmake_minimum_required(VERSION 3.15) +project(usercontainer2 LANGUAGES CXX) + +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_POSITION_INDEPENDENT_CODE ON) + +add_subdirectory(pam-module) + +# Rust backend build integration +add_custom_target(rust_backend_build + COMMAND cargo build --release --manifest-path=${CMAKE_SOURCE_DIR}/rust-backend/Cargo.toml --target-dir ${CMAKE_BINARY_DIR}/rust-backend/target + WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}/rust-backend +) + +add_subdirectory(tests/app) diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..40fcc67 --- /dev/null +++ b/LICENSE @@ -0,0 +1,200 @@ + +Apache License +Version 2.0, January 2004 +http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. + + Subject to the terms and conditions of this License, each Contributor + hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, + royalty-free, irrevocable copyright license to reproduce, prepare + Derivative Works of, publicly display, publicly perform, sublicense, + and distribute the Work and such Derivative Works in Source or Object + form. + +3. Grant of Patent License. + + Subject to the terms and conditions of this License, each Contributor + hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, + royalty-free, irrevocable (except as stated in this section) patent + license to make, have made, use, offer to sell, sell, import, and + otherwise transfer the Work, where such license applies only to those + patent claims licensable by such Contributor that are necessarily + infringed by their Contribution(s) alone or by combination of their + Contribution(s) with the Work to which such Contribution(s) was + submitted. If You institute patent litigation against any entity + (including a cross-claim or counterclaim in a lawsuit) alleging that + the Work or a Contribution incorporated within the Work constitutes + direct or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate as of + the date such litigation is filed. + +4. Redistribution. + + You may reproduce and distribute copies of the Work or Derivative + Works thereof in any medium, with or without modifications, and in + Source or Object form, provided that You meet the following conditions: + + (a) You must give any other recipients of the Work or Derivative Works + a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works that + You distribute, all copyright, patent, trademark, and attribution + notices from the Source form of the Work, excluding those notices + that do not pertain to any part of the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one of + the following places: within a NOTICE text file distributed as + part of the Derivative Works; within the Source form or + documentation, if provided with the Derivative Works; or, within + a display generated by the Derivative Works, if and wherever such + third-party notices appear. The contents of the NOTICE file are + for informational purposes only and do not modify the License. + You may add Your own attribution notices within Derivative Works + that You distribute, alongside or as an addendum to the NOTICE + text from the Work, provided that such additional attribution + notices cannot be construed as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. + + Unless You explicitly state otherwise, any Contribution intentionally + submitted for inclusion in the Work by You to the Licensor shall be + under the terms and conditions of this License, without any additional + terms or conditions. Notwithstanding the above, nothing herein shall + supersede or modify the terms of any separate license agreement you + may have executed with Licensor regarding such Contributions. + +6. Trademarks. + + This License does not grant permission to use the trade names, + trademarks, service marks, or product names of the Licensor, except as + required for describing the origin of the Work and reproducing the + content of the NOTICE file. + +7. Disclaimer of Warranty. + + Unless required by applicable law or agreed to in writing, Licensor + provides the Work (and each Contributor provides its Contributions) + on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + either express or implied, including, without limitation, any + warranties or conditions of title, non-infringement, merchantability, + or fitness for a particular purpose. You are solely responsible for + determining the appropriateness of using or redistributing the Work + and assume any risks associated with Your exercise of permissions + under this License. + +8. Limitation of Liability. + + In no event and under no legal theory, whether in tort (including + negligence), contract, or otherwise, unless required by applicable + law (such as deliberate and grossly negligent acts) or agreed to in + writing, shall any Contributor be liable to You for damages, including + any direct, indirect, special, incidental, or consequential damages of + any character arising as a result of this License or out of the use or + inability to use the Work (including but not limited to damages for + loss of goodwill, work stoppage, computer failure or malfunction, or + any and all other commercial damages or losses), even if such + Contributor has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. + + While redistributing the Work or Derivative Works thereof, You may + choose to offer, and charge a fee for, acceptance of support, + warranty, indemnity, or other liability obligations and/or rights + consistent with this License. However, in accepting such obligations, + You may act only on Your own behalf and on Your sole responsibility, + not on behalf of any other Contributor, and only if You agree to + indemnify, defend, and hold each Contributor harmless for any + liability incurred by, or claims asserted against, such Contributor + by reason of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +Copyright 2026 yono + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/README.md b/README.md new file mode 100644 index 0000000..ada90eb --- /dev/null +++ b/README.md @@ -0,0 +1,82 @@ +# PAM C++ + Rust Backend Monorepo + +This project provides a Linux PAM module written in C++ (GNU g++, C++17) that delegates authentication and logging to a Rust static library backend. The build is orchestrated by CMake, which triggers Cargo for the Rust backend. + +## Structure +- `pam-module/`: C++ PAM module source +- `rust-backend/`: Rust static library backend +- `tests/`: Integration tests + +## Build Requirements +- GNU g++ (C++17) +- CMake >= 3.15 +- Rust (cargo) +- PAM development headers + +## Build Instructions +```bash +mkdir build && cd build +cmake .. +cmake --build . +``` + +## Install +Copy the built PAM module to `/lib/security/` or `/lib64/security/` as needed. + +## Logging +Rust backend logs to `/var/log/pam_rust_backend.log` by default. + +## Safety +- Rust panics are contained and never cross FFI. +- C++ exceptions are caught before returning to PAM. + +## Extending +Add new subprojects as needed for future business logic or integrations. + +## Test Application (PAM Client) + +This repository includes a test PAM client at `tests/pam_test_app.cpp`. + +### Build the test + +```bash +mkdir -p build && cd build +cmake .. +cmake --build . +``` + +The executable will be generated at `build/tests/pam_test_app`. + +### Copy the PAM module + +After building, copy the PAM module to the system PAM module path: + +```bash +sudo cp build/pam-module/pam_module.so /lib/security/ +``` + +On some distributions, use `/lib64/security/` instead. + +### Configure `/etc/pam.d` service + +Create `/etc/pam.d/pam_test_app` with: + +```conf +auth required pam_module.so +account required pam_permit.so +``` + +You can also pass module arguments which are exposed as `argc` and `argv` to `pam_sm_authenticate`, for example: + +```conf +auth required pam_module.so debug log_path=/var/log/pam_rust_backend.log +account required pam_permit.so +``` + +### Run the test client + +```bash +./tests/pam_test_app pam_test_app +``` + +The first argument (`pam_test_app`) must match the service filename in `/etc/pam.d/pam_test_app`. diff --git a/docker/Dockerfile b/docker/Dockerfile new file mode 100644 index 0000000..3f694bd --- /dev/null +++ b/docker/Dockerfile @@ -0,0 +1,21 @@ +FROM rust:1.94-trixie AS builder + +WORKDIR /app +COPY . . + +SHELL [ "/bin/bash", "-o", "pipefail", "-c" ] + +RUN apt update && apt install -y cmake build-essential libpam-dev && \ + mkdir build && \ + pushd build && \ + cmake .. && \ + cmake --build . && \ + popd + +FROM debian:trixie + +WORKDIR /app +COPY --from=builder --chmod=644 /app/build/pam-module/pam_usercontainer.so /lib/x86_64-linux-gnu/security/pam_usercontainer.so +COPY --from=builder /app/tests/config/pam.d/mytestservice /etc/pam.d/mytestservice +COPY --from=builder /app/build/tests/app/pam_test_app /usr/local/bin/pam_test_app + diff --git a/pam-module/CMakeLists.txt b/pam-module/CMakeLists.txt new file mode 100644 index 0000000..6e40024 --- /dev/null +++ b/pam-module/CMakeLists.txt @@ -0,0 +1,25 @@ +cmake_minimum_required(VERSION 3.15) +project(pam_usercontainer LANGUAGES CXX) + +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_POSITION_INDEPENDENT_CODE ON) + +add_library(pam_usercontainer SHARED src/pam_module.cpp src/auth_client.cpp) +set_target_properties(pam_usercontainer PROPERTIES + OUTPUT_NAME "pam_usercontainer" + PREFIX "" +) +target_compile_options(pam_usercontainer PRIVATE + $<$:-Wall -Wextra> + $<$:-Wall -Wextra> +) +target_include_directories(pam_usercontainer PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/include) + +# Link to Rust staticlib +add_dependencies(pam_usercontainer rust_backend_build) +set(RUST_STATICLIB "${CMAKE_BINARY_DIR}/rust-backend/target/release/librust_backend.a") +target_link_libraries(pam_usercontainer PRIVATE ${RUST_STATICLIB}) + +# Install target +install(TARGETS pam_usercontainer DESTINATION lib/security) diff --git a/pam-module/include/rust_backend_ffi.h b/pam-module/include/rust_backend_ffi.h new file mode 100644 index 0000000..f7695b9 --- /dev/null +++ b/pam-module/include/rust_backend_ffi.h @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2026 Suyono +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + +#pragma once + +#ifdef __cplusplus +extern "C" { +#endif + +// Initialize logging +void rust_init_logging(const char* log_path); + +// Authenticate user (stub) +int rust_auth_user(const char* user, const char* password); + +// Log event (file sink) +void rust_log_event(const char* event); + +#ifdef __cplusplus +} +#endif diff --git a/pam-module/src/auth_client.cpp b/pam-module/src/auth_client.cpp new file mode 100644 index 0000000..49a839e --- /dev/null +++ b/pam-module/src/auth_client.cpp @@ -0,0 +1,26 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2026 Suyono +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + +#include "rust_backend_ffi.h" +#include "auth_client.h" +#include +// Add PAM header for status codes +#include +#include + +int auth_client_authenticate(const char* user, const char* password) { + try { + rust_init_logging(nullptr); // Ensure logger is initialized + int result = rust_auth_user(user, password); + if (result == 0) { + return PAM_SUCCESS; + } else { + return PAM_AUTH_ERR; + } + } catch (...) { + return PAM_AUTH_ERR; + } +} diff --git a/pam-module/src/auth_client.h b/pam-module/src/auth_client.h new file mode 100644 index 0000000..b4ed385 --- /dev/null +++ b/pam-module/src/auth_client.h @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2026 Suyono +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + +int auth_client_authenticate(const char* user, const char* password); diff --git a/pam-module/src/pam_module.cpp b/pam-module/src/pam_module.cpp new file mode 100644 index 0000000..ba25a4b --- /dev/null +++ b/pam-module/src/pam_module.cpp @@ -0,0 +1,38 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2026 Suyono +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + +#include +#include +#include +#include "rust_backend_ffi.h" +#include "auth_client.h" + +extern "C" { + +int pam_sm_authenticate(pam_handle_t* pamh, int flags, int argc, const char** argv) { + try { + rust_init_logging(nullptr); // Ensure logger is initialized + rust_log_event("PAM authentication attempt"); + + + const char* user = nullptr; + pam_get_user(pamh, &user, NULL); + const void* pw_ptr = nullptr; + int item_result = pam_get_item(pamh, PAM_AUTHTOK, &pw_ptr); + const char* password = (item_result == PAM_SUCCESS && pw_ptr) ? static_cast(pw_ptr) : nullptr; + int result = auth_client_authenticate(user, password); + return result; + } catch (...) { + // Log and return PAM error + return PAM_AUTH_ERR; + } +} + +int pam_sm_setcred(pam_handle_t*, int, int, const char**) { + return PAM_SUCCESS; +} + +} diff --git a/rust-backend/Cargo.lock b/rust-backend/Cargo.lock new file mode 100644 index 0000000..348ca76 --- /dev/null +++ b/rust-backend/Cargo.lock @@ -0,0 +1,16 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "rust-backend" +version = "0.1.0" +dependencies = [ + "once_cell", +] diff --git a/rust-backend/Cargo.toml b/rust-backend/Cargo.toml new file mode 100644 index 0000000..6b00748 --- /dev/null +++ b/rust-backend/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "rust-backend" +version = "0.1.0" +edition = "2021" + +[lib] +crate-type = ["staticlib"] + +[dependencies] +once_cell = "1.18" + +[profile.release] +panic = "unwind" + +[profile.dev] +panic = "unwind" diff --git a/rust-backend/src/auth.rs b/rust-backend/src/auth.rs new file mode 100644 index 0000000..fac1981 --- /dev/null +++ b/rust-backend/src/auth.rs @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2026 Suyono +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + +pub fn authenticate(user: &str, password: &str) -> Result<(), ()> { + // Stub: always fail if user == "fail", else succeed + if user == "fail" { + Err(()) + } else { + Ok(()) + } +} diff --git a/rust-backend/src/error.rs b/rust-backend/src/error.rs new file mode 100644 index 0000000..0a69b66 --- /dev/null +++ b/rust-backend/src/error.rs @@ -0,0 +1,10 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2026 Suyono +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + +pub const SUCCESS: i32 = 0; +pub const ERR_INVALID_INPUT: i32 = -1; +pub const ERR_AUTH_FAILED: i32 = -2; +pub const ERR_PANIC: i32 = -99; diff --git a/rust-backend/src/lib.rs b/rust-backend/src/lib.rs new file mode 100644 index 0000000..854dbe0 --- /dev/null +++ b/rust-backend/src/lib.rs @@ -0,0 +1,62 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2026 Suyono +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + +use std::ffi::{CStr, CString}; +use std::os::raw::c_char; +use std::panic::{catch_unwind, AssertUnwindSafe}; + +mod auth; +mod logging; +mod error; + +#[no_mangle] +pub extern "C" fn rust_init_logging(log_path: *const c_char) { + let _ = catch_unwind(AssertUnwindSafe(|| { + let path = unsafe { + if log_path.is_null() { + "/var/log/pam_rust_backend.log" + } else { + CStr::from_ptr(log_path).to_str().unwrap_or("/var/log/pam_rust_backend.log") + } + }; + logging::init_logger(path); + })); +} + +#[no_mangle] +pub extern "C" fn rust_log_event(event: *const c_char) { + let _ = catch_unwind(AssertUnwindSafe(|| { + let msg = unsafe { + if event.is_null() { + return; + } + CStr::from_ptr(event).to_str().unwrap_or("") + }; + logging::log_event(msg); + })); +} + +#[no_mangle] +pub extern "C" fn rust_auth_user(user: *const c_char, password: *const c_char) -> i32 { + catch_unwind(AssertUnwindSafe(|| { + let u = unsafe { + if user.is_null() { + return error::ERR_INVALID_INPUT; + } + CStr::from_ptr(user).to_str().unwrap_or("") + }; + let p = unsafe { + if password.is_null() { + return error::ERR_INVALID_INPUT; + } + CStr::from_ptr(password).to_str().unwrap_or("") + }; + match auth::authenticate(u, p) { + Ok(_) => error::SUCCESS, + Err(_) => error::ERR_AUTH_FAILED, + } + })).unwrap_or(error::ERR_PANIC) +} diff --git a/rust-backend/src/logging.rs b/rust-backend/src/logging.rs new file mode 100644 index 0000000..e3188b7 --- /dev/null +++ b/rust-backend/src/logging.rs @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2026 Suyono +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + +use std::fs::{OpenOptions, File}; +use std::io::Write; +use std::sync::Mutex; +use once_cell::sync::OnceCell; + +static LOGGER: OnceCell> = OnceCell::new(); + +pub fn init_logger(path: &str) { + if LOGGER.get().is_some() { + // Already initialized, do nothing + return; + } + let file = OpenOptions::new() + .create(true) + .append(true) + .open(path) + .expect("Failed to open log file"); + LOGGER.set(Mutex::new(file)).ok(); +} + +pub fn log_event(event: &str) { + if let Some(logger) = LOGGER.get() { + let mut file = logger.lock().unwrap(); + writeln!(file, "{}", event).ok(); + } +} diff --git a/tests/app/CMakeLists.txt b/tests/app/CMakeLists.txt new file mode 100644 index 0000000..127201f --- /dev/null +++ b/tests/app/CMakeLists.txt @@ -0,0 +1,7 @@ +cmake_minimum_required(VERSION 3.15) +project(pam_test_app LANGUAGES CXX) + +add_executable(pam_test_app pam_test_app.cpp) +target_link_libraries(pam_test_app PRIVATE pam) + +install(TARGETS pam_test_app RUNTIME DESTINATION bin) diff --git a/tests/app/pam_test_app.cpp b/tests/app/pam_test_app.cpp new file mode 100644 index 0000000..8909099 --- /dev/null +++ b/tests/app/pam_test_app.cpp @@ -0,0 +1,95 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2026 Suyono +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + +#include +#include + +#include +#include +#include +#include + +namespace { + +struct AppData { + const char* password; +}; + +int conversation(int num_msg, const struct pam_message** msg, struct pam_response** resp, void* appdata_ptr) { + if (num_msg <= 0 || msg == nullptr || resp == nullptr) { + return PAM_CONV_ERR; + } + + auto* app_data = static_cast(appdata_ptr); + auto* replies = static_cast(calloc(static_cast(num_msg), sizeof(pam_response))); + if (replies == nullptr) { + return PAM_BUF_ERR; + } + + for (int i = 0; i < num_msg; ++i) { + replies[i].resp_retcode = 0; + replies[i].resp = nullptr; + + switch (msg[i]->msg_style) { + case PAM_PROMPT_ECHO_OFF: + case PAM_PROMPT_ECHO_ON: + if (app_data != nullptr && app_data->password != nullptr) { + replies[i].resp = strdup(app_data->password); + if (replies[i].resp == nullptr) { + free(replies); + return PAM_BUF_ERR; + } + } + break; + case PAM_ERROR_MSG: + case PAM_TEXT_INFO: + break; + default: + free(replies); + return PAM_CONV_ERR; + } + } + + *resp = replies; + return PAM_SUCCESS; +} + +} // namespace + +int main(int argc, char** argv) { + if (argc < 3) { + std::cerr << "Usage: " << argv[0] << " [password]" << std::endl; + return 2; + } + + const std::string service = argv[1]; + const std::string user = argv[2]; + const char* password = (argc >= 4) ? argv[3] : ""; + + AppData app_data{password}; + pam_conv conv{conversation, &app_data}; + pam_handle_t* pamh = nullptr; + + int rc = pam_start(service.c_str(), user.c_str(), &conv, &pamh); + if (rc != PAM_SUCCESS) { + std::cerr << "pam_start failed: " << pam_strerror(pamh, rc) << std::endl; + return 1; + } + + rc = pam_authenticate(pamh, 0); + if (rc == PAM_SUCCESS) { + std::cout << "Authentication succeeded" << std::endl; + } else { + std::cout << "Authentication failed: " << pam_strerror(pamh, rc) << std::endl; + } + + const int end_rc = pam_end(pamh, rc); + if (end_rc != PAM_SUCCESS) { + std::cerr << "pam_end failed: " << pam_strerror(pamh, end_rc) << std::endl; + } + + return (rc == PAM_SUCCESS) ? 0 : 1; +} diff --git a/tests/config/pam.d/mytestservice b/tests/config/pam.d/mytestservice new file mode 100644 index 0000000..6c54f4d --- /dev/null +++ b/tests/config/pam.d/mytestservice @@ -0,0 +1 @@ +auth required pam_usercontainer.so