# Testing pam_authforge_pending.so This module gates first-login enrollment. Its full behavior is impossible to exercise from a unit test because PAM modules run inside a real PAM stack that needs root to install. The recipe below runs in a VM or container with `libpam0g-dev` and `pamtester` installed. ## Build + install ```bash sudo apt-get install -y libpam0g-dev libargon2-dev pamtester sudo make -C pam install sudo install -D -m 0644 pam/test/authforge.pamd /etc/pam.d/authforge ``` `libargon2-dev` is required for the recovery-mode verification path (Phase 12). `make install` drops the `.so` into `/usr/lib/$(dpkg-architecture -qDEB_HOST_MULTIARCH)/security/`. ## Smoke test 1: pending flag present → denied ```bash sudo mkdir -p /var/lib/authforge/pending sudo touch /var/lib/authforge/pending/$USER pamtester -v authforge "$USER" authenticate # Expect: "Account setup incomplete..." printed and a non-zero exit code. ``` ## Smoke test 2: pending flag cleared → falls through ```bash sudo rm /var/lib/authforge/pending/$USER pamtester -v authforge "$USER" authenticate # Expect: success (the test PAM stack file falls through to pam_unix after # our PAM_IGNORE; pamtester will prompt for the user's password). ``` ## Smoke test 3: path-traversal regression These usernames must all be rejected with a logged error and `PAM_AUTH_ERR`: ```bash for u in "../etc" "a/b" "" "." ".." "x..y"; do pamtester -v authforge "$u" authenticate 2>&1 | head -3 echo "---" done ``` The module logs via `pam_syslog` without setting its own `openlog` ident, so journal entries inherit the calling process's syslog identifier: `pamtester` here, `sudo` / `sshd` / `gdm` / `login` in production. The module name appears in the message body, so the always-works query is to grep for the literal string: ```bash journalctl --since "10 minutes ago" --no-pager | grep authforge_pending ``` For pamtester runs specifically, `journalctl -t pamtester` works too. Older systems that route `pam_syslog` to `/var/log/auth.log` show the same lines there. ## Smoke test 4: recovery-code path (Phase 12) The module's recovery branch activates when invoked with `mode=recovery` in the PAM stack. The pam-auth-update profile rendered by Phase 4 puts this line first in the auth stack — a successful recovery code short-circuits the rest of the auth chain via `[success=done default=ignore]`. To exercise it manually with `pamtester` against a custom stack: ```bash # 1. Issue a code via the daemon and stash the plaintext. CODE=$(sudo authforgectl recovery generate "$USER" | tr -d '\n') echo "issued $CODE" # 2. Build a PAM stack file that runs ONLY the recovery-mode module. sudo tee /etc/pam.d/authforge-recovery <<'EOF' auth [success=done default=ignore] pam_authforge_pending.so mode=recovery auth required pam_deny.so EOF # 3. Verify a matching code authenticates. echo "$CODE" | pamtester -v authforge-recovery "$USER" authenticate # Expect: PAM_SUCCESS, /var/lib/authforge/recovery/$USER deleted, and # /var/lib/authforge/pending/$USER now contains re_enroll: true. # 4. Verify a non-matching code falls through to pam_deny. echo "$CODE" | pamtester -v authforge-recovery "$USER" authenticate # Expect: failure (pam_deny) — the recovery file was already consumed. # 5. Cleanup. sudo rm /etc/pam.d/authforge-recovery /var/lib/authforge/pending/$USER ``` The recovery file is at `/var/lib/authforge/recovery/` mode 0600. The two-line format is `\n\n`. ## CI status CI cannot run `pamtester` against a real PAM stack without root and a VM, so this is the last manual gate before the Phase 14 PPA build. The CI pipeline only verifies that `make -C pam` produces a valid ELF.