diff --git a/pam/pam_authforge_pending.c b/pam/pam_authforge_pending.c index ae2835e..795e8ed 100644 --- a/pam/pam_authforge_pending.c +++ b/pam/pam_authforge_pending.c @@ -1,14 +1,78 @@ +/* + * pam_authforge_pending.so — backstop for AuthForge first-login enrollment. + * + * Behavior: + * 1. Read PAM_USER. + * 2. Reject usernames that would let the path computation escape + * /var/lib/authforge/pending/. + * 3. stat() /var/lib/authforge/pending/. + * - present -> emit user-facing message + return PAM_AUTH_ERR. + * - ENOENT -> return PAM_IGNORE (let the rest of the stack decide). + * - other -> log + return PAM_AUTH_ERR (fail closed). + * + * Recovery codes (Phase 12) wire into a separate hook that's a no-op here. + */ + #define PAM_SM_AUTH #include #include -#include + +#include #include +#include +#include +#include + +#define PENDING_DIR "/var/lib/authforge/pending/" +#define USER_MSG "Account setup incomplete. Please complete enrollment in the Authentication app." + +static int username_is_safe(const char *u) { + if (u == NULL || u[0] == '\0') return 0; + if (strcmp(u, ".") == 0 || strcmp(u, "..") == 0) return 0; + for (const char *p = u; *p; ++p) { + if (*p == '/') return 0; + } + /* Reject any '..' substring as a defense-in-depth measure. */ + if (strstr(u, "..") != NULL) return 0; + return 1; +} + +static int pending_flag_present(const char *user) { + char path[1024]; + int n = snprintf(path, sizeof(path), "%s%s", PENDING_DIR, user); + if (n < 0 || (size_t)n >= sizeof(path)) return -1; + struct stat st; + if (stat(path, &st) == 0) return 1; + if (errno == ENOENT) return 0; + return -1; +} PAM_EXTERN int pam_sm_authenticate(pam_handle_t *pamh, int flags, int argc, const char **argv) { (void)flags; (void)argc; (void)argv; - pam_syslog(pamh, LOG_INFO, "authforge_pending: stub - allowing"); - return PAM_IGNORE; /* implemented in Phase 6 */ + + const char *user = NULL; + if (pam_get_user(pamh, &user, NULL) != PAM_SUCCESS || user == NULL) { + pam_syslog(pamh, LOG_ERR, "authforge_pending: pam_get_user failed"); + return PAM_AUTH_ERR; + } + if (!username_is_safe(user)) { + pam_syslog(pamh, LOG_ERR, "authforge_pending: rejected unsafe username"); + return PAM_AUTH_ERR; + } + + int present = pending_flag_present(user); + if (present < 0) { + pam_syslog(pamh, LOG_ERR, "authforge_pending: stat error for %s (errno=%d)", user, errno); + return PAM_AUTH_ERR; /* fail closed */ + } + if (present == 0) { + return PAM_IGNORE; + } + + pam_error(pamh, "%s", USER_MSG); + pam_syslog(pamh, LOG_INFO, "authforge_pending: denied %s (pending flag present)", user); + return PAM_AUTH_ERR; } PAM_EXTERN int pam_sm_setcred(pam_handle_t *pamh, int flags,