Files
authforge/pam/TESTING.md
michael 56ba4dd924 feat(pam): recovery-code mode with Argon2id verify and pending re-enroll handoff
C-side:
* New mode=recovery argv branch in pam_sm_authenticate. Reads the two-line
  /var/lib/authforge/recovery/<user> file, argon2_verify against PAM_AUTHTOK,
  on match unlinks the file (one-shot) and writes pending(re_enroll=true).
  Always returns PAM_IGNORE on failure paths so a missing/wrong code never
  blocks normal auth.
* Makefile links -largon2 alongside -lpam.

Daemon-side:
* policy_apply::render_profile renders the recovery line first in the auth
  stack with [success=done default=ignore] — successful recovery short-
  circuits the rest, missing/wrong code falls through.
* New policy_apply test asserts the recovery line precedes the default
  backstop.

Doc:
* pam/TESTING.md adds libargon2-dev to the build prereqs and a new
  Smoke test 4 walking through the manual recovery-code flow.

C compile gate (make -C pam) requires libargon2-dev — flagged as a
deferred verification step until a host with the dev package is available.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 09:49:44 -07:00

3.3 KiB

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

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

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

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:

for u in "../etc" "a/b" "" "." ".." "x..y"; do
    pamtester -v authforge "$u" authenticate 2>&1 | head -3
    echo "---"
done

Logs show up via journalctl -t authforge_pending (or wherever your distribution routes pam_syslog — typically /var/log/auth.log).

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:

# 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/<user> mode 0600. The two-line format is <expires_unix>\n<argon2id-PHC>\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.