Skip to content

wolfsshd: enforce shadow password and account aging - #1184

Open
yosuke-wolfssl wants to merge 1 commit into
wolfSSL:masterfrom
yosuke-wolfssl:fix/f_10577
Open

wolfsshd: enforce shadow password and account aging#1184
yosuke-wolfssl wants to merge 1 commit into
wolfSSL:masterfrom
yosuke-wolfssl:fix/f_10577

Conversation

@yosuke-wolfssl

Copy link
Copy Markdown
Contributor

Problem

CheckPasswordUnix() read only shadowInfo->sp_pwdp from the shadow entry it
looked up. The aging fields — sp_expire, sp_lstchg, sp_max — were never
examined, so an account past its expiration date, or a password past its
maximum age, authenticated normally as long as the stored hash matched. Any
account an administrator had locked with chage -E or aged out still had a
working password over SSH. Closes f-10577.

Fix (apps/wolfsshd/auth.c)

New IsShadowExpired() denies a login when any of these holds:

Condition Meaning
sp_expire >= 0 && today >= sp_expire account expiration date reached
sp_lstchg == 0 administrator forced a change at next login
sp_lstchg > 0 && sp_max >= 0 && today - sp_lstchg >= sp_max password aged out

Negative fields leave the matching check off, which is how an unset shadow
field arrives from getspnam(). A clock failure denies only entries that
actually carry aging.

  • Denied as an auth failure, not a protocol error: wolfsshd has no
    SSH_MSG_USERAUTH_PASSWD_CHANGEREQ, and DoUserAuthRequestPassword()
    already rejects password-change requests per RFC 4252 §8.
  • The override runs after the hash compare, so crypt() still executes and
    an expired account costs the same as a live one.
  • Compiled under HAVE_SHADOW and only without PAM, matching its caller.

Two deliberate choices worth a reviewer's eye: expiry takes effect on the
stored date, matching shadow-utils/pam_unix (OpenSSH uses the day after);
and both checks live in the password path, so public-key login to an expired
account is still permitted.

Tests (apps/wolfsshd/test/test_configuration.c)

test_IsShadowExpired walks a table of aging fields and day counts, including
both sides of each boundary; test_CheckPasswordUnix_expired puts a correct
password through an expired account. The CheckPasswordUnix tests now share
one driver, and the three fail-closed shadow-lookup tests collapse into one
table-driven test_CheckPasswordUnix_failClosed.

Verification

  • Linux --enable-all --enable-ossh-certs, -O2 -Werror: clean. GCC preflight
    sweep, 6 configs: clean.
  • 77 unit tests pass; make check 10 pass / 2 skip / 0 fail.
  • Mutation checks: disabling the call-site denial, dropping any one aging rule,
    or shifting either boundary a day in either direction each fails a test.
  • Cross-checked against real chage -E / chage -M shadow entries.
  • No ASan run — the change adds integer comparisons, no buffer or pointer
    arithmetic.

Not in this PR: --with-pam builds fail on master already (CheckPasswordPAM()
does not match CallbackCheckPassword), so the PAM path is untouched here.

@yosuke-wolfssl yosuke-wolfssl self-assigned this Aug 20, 2026
Copilot AI lite review requested due to automatic review settings August 20, 2026 06:35

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR updates wolfsshd’s Unix shadow-password authentication to enforce shadow account/password aging rules (account expiry, forced password change, and maximum password age) so expired/aged accounts can’t continue authenticating via password even if the hash matches.

Changes:

  • Add IsShadowExpired() and wire it into CheckPasswordUnix() to deny password authentication after a successful hash compare when shadow aging indicates expiration.
  • Refactor/expand unit tests to cover both the new aging rules and the CheckPasswordUnix() expired-account behavior.
  • Consolidate several shadow failure-mode tests into a table-driven helper for maintainability.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 3 comments.

File Description
apps/wolfsshd/auth.c Adds shadow-aging evaluation (IsShadowExpired) and enforces expiry in the password auth flow post-hash-compare.
apps/wolfsshd/auth.h Exposes IsShadowExpired for unit tests under shadow-enabled builds.
apps/wolfsshd/test/test_configuration.c Refactors CheckPasswordUnix shadow tests, and adds direct coverage for shadow aging rules + an expired-account password test.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread apps/wolfsshd/auth.h
Comment thread apps/wolfsshd/test/test_configuration.c
Comment thread apps/wolfsshd/auth.c
- IsShadowExpired() in auth.c returns 1 when a shadow entry's
  sp_expire date has arrived, its sp_lstchg is 0, or the day is at or
  past sp_lstchg + sp_max. Negative fields leave the matching check
  off; a negative day count, standing for an unavailable clock,
  denies the entries that carry aging. WSSHD_SECS_PER_DAY converts
  WTIME() into the unit those fields use. The helper is compiled
  under HAVE_SHADOW and !WOLFSSH_USE_PAM, as its caller is.
- CheckPasswordUnix() runs the shadow entry it looked up through the
  helper and, after an otherwise successful hash compare, logs the
  denial and returns WSSHD_AUTH_FAILURE.
- auth.h declares IsShadowExpired() for the unit test build.
- test_configuration.c adds test_IsShadowExpired() over a table of
  aging fields and day counts, and test_CheckPasswordUnix_expired()
  for the denial of a correct password.
- The CheckPasswordUnix() tests share one driver,
  wsshd_test_CheckPasswordUnixCase(), with the crypt() setup in
  wsshd_test_LoadShadowHash() and the three fail-closed shadow
  lookups gathered into test_CheckPasswordUnix_failClosed().

Issue: F-10577
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants