146 Commits

Author SHA1 Message Date
Pierre Warnier 698eaf7cf3 make install: default to standalone per-tool binaries (#138)
Switch the default install to build and install 14 standalone per-tool
binaries with a least-privilege setuid layout matching GNU shadow-utils.
Only passwd/chfn/chsh/newgrp are setuid-root; the other 10 tools are 0755.

The previous multicall install is preserved as `make install-multicall`
for container/embedded use cases where the ~14x disk savings matter and
the enlarged setuid attack surface is acceptable.

The old layout chmod'd symlinks pointing to the multicall binary, which
(because chmod follows symlinks) marked the underlying ELF setuid-root.
That meant all 14 tools ran with euid=0 when invoked via symlink. Each
tool's internal getuid() check was defense-in-depth, not OS-level least
privilege. The standalone layout gives the latter.

Raised by @oech3 in uutils/coreutils#11828.
2026-04-15 14:19:21 +02:00
Pierre Warnier e8208d880c Merge pull request #137 from oech3/patch-1
ci.yml: simplify by pre-install toolchain
2026-04-15 14:09:51 +02:00
oech3 e684c218d0 ci.yml: switch to non-versioned URL 2026-04-15 20:37:28 +09:00
oech3 063d47c37e ci.yml: simplify by pre-install toolchain 2026-04-15 17:50:51 +09:00
Pierre Warnier 279637004e Merge pull request #136 from shadow-utils-rs/fix/review-round2-all-findings
review: fix all findings from comprehensive code review
2026-04-04 07:03:47 +02:00
Pierre Warnier 0ad043d14a review: address all 5 Copilot review comments
- userdel: scope SignalBlocker to file mutations only, drop before
  home directory removal so long-running deletions stay interruptible
- usermod: drop SignalBlocker before recursive_chown so large home
  trees remain interruptible
- useradd: drop SignalBlocker after lock release, before subid/home
  creation steps that may be long-running
- atomic: use PhantomData<Rc<()>> instead of PhantomData<*const ()>
  for clearer !Send/!Sync opt-out on UmaskGuard
- newgrp: replace harden_process() with suppress_core_dumps() +
  sanitized_env() to avoid leaking RLIMIT_FSIZE into exec'd shell
2026-04-04 06:59:39 +02:00
Pierre Warnier 92783375aa review: fix all findings from comprehensive code review
Security fixes:
- newgrp: add initgroups() before execv to reset supplementary groups (H-3)
- pam: use Zeroizing<String> for password buffers on all exit paths (M-5)
- atomic: retry-once on stale tmp file from crashed run (M-3)
- atomic: UmaskGuard opts out of Send/Sync via PhantomData (L-2)
- lock: flush + fsync in write_pid_file before hard_link (L-7)

Deduplication (~300 lines removed):
- Centralize caller_is_root(), current_username(), SignalBlocker in
  shadow_core::hardening — remove 10+5+5 private copies across tools
- Add SignalBlocker to all 6 remaining mutating tools (M-4)
- Remove dead CantSort variant from grpck, fix pwck allow (M-8)
- Remove dead read_passwd from test_chfn (M-8)

Binary cleanup:
- shadow-rs multicall: use ExitCode instead of process::exit (M-6)
- completions: use ExitCode + Result pattern, remove unwrap/exit (H-3)

Documentation:
- crypt: thread-safety warning on public API (H-1)
- shadow: clarify unlock() behavior for * password (H-2)
- validate: document $ rejection deviation from GNU (L-4)
- selinux/audit: note shell-out implementation status (L-6)
- chpasswd/chage tests: TODO for --prefix integration (M-8)
- userdel: document inline skip_unless_root reason (L-9)

Testing:
- Add fuzz targets for group and gshadow parsers (M-7)
- skel: use create_dir for tighter error detection (L-8)
- crypt: compile-time modulo bias assertion (L-1)
2026-04-03 19:27:25 +02:00
Pierre Warnier 046ec1de2b Merge pull request #135 from shadow-utils-rs/fix/128-133-remaining-review
fix: address remaining review findings #128 #131 #133
2026-04-03 18:20:15 +02:00
Pierre Warnier d74a1e667f review: address all 7 Copilot review comments
- shadow.rs: use ShadowError::Other instead of Parse for clock error
- crypt.rs: use getrandom(2) syscall instead of /dev/urandom (works in chroot)
- crypt.rs: reject rounds parameter for yescrypt in generate_salt()
- crypt.rs: detect unsupported methods by verifying result prefix (musl compat)
- crypt.rs: serialize tests with Mutex (crypt(3) uses static buffer)
- crypt.rs: add 6 round-trip tests for hash_password/verify_password
- chpasswd.rs: validate --sha-rounds range, reject -m unconditionally
- chpasswd.rs: strip rounds for yescrypt before calling hash_password
- chpasswd: add crypt feature to shadow-core dependency
2026-04-03 18:14:57 +02:00
Pierre Warnier 20ee217096 crypt: detect unsupported hash methods from crypt(3)
crypt(3) returns "*0" or "*1" when the requested method is unsupported
(e.g. yescrypt on musl libc). Previously this was returned as a valid
hash, causing assertion failures in tests on Alpine.

Now hash_password() checks for the "*" error prefix and returns
ShadowError::Auth, allowing callers to handle gracefully.
2026-04-03 18:13:08 +02:00
Pierre Warnier ca61f72c3f review: address all 7 Copilot review comments
Most feedback was already addressed in prior commits. The one remaining
fix: generate_salt() now rejects rounds for yescrypt with an error
instead of silently ignoring them.

Already addressed:
- ShadowError::Parse → Other for clock errors
- Explicit sha_rounds validation (range check, error on invalid)
- Reject -m (MD5) unconditionally, not silently ignored
- getrandom(2) syscall instead of /dev/urandom (chroot-safe)
- Strip rounds for yescrypt at caller level
- Round-trip tests for hash_password + verify_password
2026-04-03 18:12:36 +02:00
Pierre Warnier 96f9c40ca3 tests: deduplicate skip_unless_root() into common module
Replace 13 local copies across all test files with a shared import
from tests/common/mod.rs. Fix getuid/geteuid inconsistency in
test_useradd.rs and test_userdel.rs (effective UID is correct for
setuid-root tools).

Fixes #133
2026-04-03 17:45:10 +02:00
Pierre Warnier eea6b02d2b chpasswd: implement plaintext password hashing
Add hash_password() and CryptMethod to shadow-core::crypt alongside
the existing verify_password(). Supports SHA-256, SHA-512, and yescrypt;
rejects MD5 and DES as insecure.

chpasswd now accepts plaintext mode (the default) and hashes passwords
via crypt(3) before writing. Previously only -e (pre-encrypted) worked.

Fixes #128
2026-04-03 17:44:19 +02:00
Pierre Warnier b4c3951d1a shadow-core: make days_since_epoch() return Result
The function returned 0 on clock error, silently corrupting shadow
entries with sp_lstchg=0 (which means "force password change").
Now returns Result<i64, ShadowError> so callers handle the error.

Deduplicate local copies in useradd and chpasswd — both now delegate
to the shadow-core version.

Fixes #131
2026-04-03 17:43:51 +02:00
Pierre Warnier 76bed9fcad Merge pull request #134 from shadow-utils-rs/fix/119-133-review-findings
security: fix 10 review findings (2 critical, 5 high, 1 medium)
2026-04-03 17:25:23 +02:00
Pierre Warnier c05f095f7e review: address all 6 Copilot review comments
- sysroot: add try_resolve() that returns Option for user-controlled
  paths; resolve() uses unreachable! since only called with hardcoded
  paths like "/etc/passwd"
- useradd: clamp subid start to at least 100_000 even when existing
  entries are below that threshold
- grpck, pwck, userdel, usermod: use `let _ =` for harden_process()
  since these tools don't spawn child processes
2026-04-03 17:19:54 +02:00
Pierre Warnier 6af298561c security: fix 10 review findings across 11 files
CRITICAL fixes:
- crypt: constant-time hash comparison via `subtle::ConstantTimeEq`
  to prevent timing side-channel attacks (#119)
- useradd: hold all locks (passwd, shadow, group) during entire
  mutation window to prevent TOCTOU races (#120)

HIGH fixes:
- newgrp: read shell from passwd entry instead of tainted $SHELL (#121)
- sysroot: reject path traversal (.. components) in --root/--prefix (#122)
- userdel, usermod: replace .expect() with proper UResult error
  handling — no panics in setuid-root code (#123)
- userdel: canonicalize home path before recursive delete (#124)
- newgrp: zeroize password memory on drop via Zeroizing<String> (#125)
- grpck, pwck, userdel, usermod: add harden_process() call (#127)

MEDIUM fix:
- useradd: find next available subid range instead of fixed formula,
  prevents overlapping container ID namespaces (#129)
2026-04-03 17:06:56 +02:00
Pierre Warnier 347349721a README: credit Claude Code as main writer, add links for all AI tools 2026-04-03 16:19:35 +02:00
Pierre Warnier 4bf9338f48 Merge pull request #118 from shadow-utils-rs/docs/audit-fixes
docs: fix stale claims, add missing usermod -p flag
2026-04-03 15:33:09 +02:00
Pierre Warnier 0a33f2186d review: address 5 Copilot review comments
- Fix function name: sanitize_env() → sanitized_env() / harden_process()
- Fix SIGINT handler: was removed, not implemented
- Fix user enumeration: early permission check, not constant-time PAM
- Fix Rust module paths: shadow-core → shadow_core (crate vs module)
2026-04-03 15:27:58 +02:00
Pierre Warnier 4259dc7cd0 docs: fix stale claims, add missing -p flag to usermod man page
- README: fix passwd flag count (17→16), add Landlock mention, add
  Hardened goal, update usermod description with -p, fix past tense
- CONTRIBUTING: fix unsafe description to match actual workspace policy
- CHANGELOG: add [Unreleased] section for post-v0.1.0 changes
- OPENBSD-REFERENCE: move all implemented items to correct section
- SECURITY-HARDENING: mark all 21 items as implemented
- man/usermod: add -p/--password flag documentation
2026-04-03 15:16:41 +02:00
Pierre Warnier 05b22d2b46 Merge pull request #117 from shadow-utils-rs/meta/uutils-conventions
meta: prepare repo for uutils ecosystem integration
2026-04-03 15:06:33 +02:00
Pierre Warnier fdb6d46a05 review: address 2 Copilot review comments
- Add homepage.workspace to root package (missing vs subcrates)
- Gate Docker test-docker job to push only (seccomp:unconfined
  and SYS_ADMIN are unsafe for untrusted PR contexts)
2026-04-03 15:01:26 +02:00
Pierre Warnier 5a457b8781 ci: add Docker multi-distro tests, renovate, rust-toolchain
- Add Docker test matrix (debian/alpine/fedora) to GitHub Actions CI
- Add renovate.json for automated dependency updates
- Add rust-toolchain.toml for contributor convenience
2026-04-03 14:45:43 +02:00
Pierre Warnier 95d2b2478a docs: add CI/MSRV badges, fix stale reference
- Add CI and MSRV badges to README
- Remove "(when created)" from CODE_OF_CONDUCT.md reference in CONTRIBUTING.md
2026-04-03 14:45:17 +02:00