3 Commits
Author SHA1 Message Date
Pierre Warnier 02fefda42c ci: compare against GNU for real, pin the actions by digest, one make check (#275)
* ci: pin the actions by digest

A tag is a moving pointer. Whoever controls the action's repository can
repoint v7 at any commit, and every workflow in the project runs it on
the next push with write access to the checkout. A digest cannot be
repointed.

The tag stays in a trailing comment so the version is still readable, and
Renovate's pinGitHubActionDigests preset is enabled so the digests are
kept current the same way every other dependency is -- as a pull request
that CI has run, rather than silently.

* tests: make the GNU comparison real, and run it in CI

The suite compared full output for exactly two commands and, for the
other twelve, only that both tools exit 0 on --help. It ran nowhere.

It now compares what is actually a contract. 'passwd -S' and 'chage -l'
are parsed by monitoring and provisioning scripts, so their output is
compared byte for byte across eleven aging configurations -- including
the cases this project got wrong until recently: a last change of 0, the
threshold above which a maximum age reads never, and cleared fields. Exit
codes are compared on ten error paths. --help output is deliberately not
compared: ours comes from clap and is meant to differ.

One difference is deliberate and now asserted as such, so it cannot be
mistaken for a regression the day it changes by accident. GNU parses
dates leniently and rolls them over -- 'chage -d 2025-02-29' stores 1
March and '-d 2025-13-01' stores 1 January 2026. An administrator who
typed one of those made a mistake, and silently storing a different date
than the one they wrote is a poor answer for a field that decides when an
account stops working. We refuse it; chage(1) says so.

38 comparisons, all as expected, and a CI job runs them.

'make check' is now the single definition of what CI gates on -- fmt,
clippy with and without pam, and the tests in both configurations. The
pre-push hook and CONTRIBUTING refer to it instead of each carrying their
own copy of the command list, and the two hook systems are documented as
the non-overlapping things they are.
2026-09-04 16:58:54 +02:00
Pierre Warnier 9cd5eef4ac Install layout, aarch64 archives, pinned images, and a pre-push hook worth keeping (#272)
* hooks: stop reproducing CI in pre-push

The hook ran fmt, clippy and the test suite on Debian, Alpine and Fedora
in series, on every push. That is several minutes for a push that may
carry nothing: pushing the 0.2.2 tag moved one object and still blocked
for over two minutes. The predictable result was that every push in the
last batch of work used --no-verify, and a hook that is always skipped
protects nothing while still costing time whenever someone forgets to
skip it.

It now runs the workspace tests once, on Debian, and returns immediately
for a push that carries no new commits -- a tag, a branch deletion, or a
branch the remote already has. git hands the hook one line per ref on
stdin; the ranges it names are asked for a single commit, and if there
are none there is nothing to test.

Everything the hook dropped is already a required check on the pull
request, running in parallel with retries: fmt, clippy and the tests with
and without pam, MSRV, cargo-deny, the static musl archive, the
three-distro matrix as root, and the end-to-end deployment image.

Closes #231.

* install: put the user-facing tools in bin, not sbin

All fourteen binaries went to $(PREFIX)/sbin, which is not on a normal
user's PATH: passwd was 'command not found' for exactly the people it is
setuid for. GNU shadow splits them, verified against the Debian package
-- passwd, chage, chfn, chsh and newgrp in /usr/bin, the other nine in
/usr/sbin. Both install targets do the same now, and the multicall
symlinks in bin point at the absolute path of the binary in sbin. chage
is in bin for 'chage -l', the one mode a user may run on their own
account.

The deployment suite follows each tool to the directory it belongs in,
and asserts that all five are reachable by name for an unprivileged
user, which is the property the split exists for.

make test also runs the suite with pam now. install ships binaries built
with it, so a run without it covered a build nobody ships.

* docker: pin the base images, drop the unused nextest download

rust:latest, rust:alpine and fedora:latest meant a new toolchain or
distro release landed on main with no pull request and no signal, and
clippy gains lints between Rust releases. They are pinned to what the
images resolve to today -- rust 1.98 on Trixie and Alpine 3.23, Fedora 44
-- and Renovate's Dockerfile manager proposes the bumps, so a toolchain
change arrives as a reviewable PR that CI has run.

Each image also downloaded cargo-nextest from get.nexte.st/latest on
every build: unpinned, unverified, and never invoked -- nothing in the
repository runs 'cargo nextest'. cargo-deny, which is used, keeps its
pinned version and checksum.

* deny: allow exactly the licences the graph uses

The allow list carried ISC, BSD-2-Clause, CC0-1.0 and Zlib, none of which
appears anywhere in the dependency graph. A list wider than the graph
means a crate arriving under a new licence passes silently; the point of
the check is that such an arrival is a decision someone records.

It is now MIT, Apache-2.0, BSD-3-Clause (subtle, for the constant-time
comparison in shadow-core::crypt) and Unicode-3.0 (tinystr and
unicode-ident, via uucore's fluent stack), taken from
'cargo deny --all-features list'.

Two crates carry a copyleft option in a dual licence and are accepted
under the other half; that is now written down rather than left for the
next reader to work out. All four checks pass with the narrower list.

* ci: run the advisory workflow on a schedule only

It also ran on every push touching a manifest or the lockfile, where the
Cargo Deny job in ci.yml had already checked the same advisory database
alongside licences and bans. Two tools reporting the same finding on the
same commit is noise.

What the pull request job cannot do is notice an advisory published
against code nobody has touched, which is the reason to keep a scheduled
run. That is now all this workflow does, plus manual dispatch.

* packaging: remove the RPM spec and debian directory

Neither can build. debian/ has no changelog at all, so dpkg-buildpackage
stops before it starts:

    dpkg-buildpackage: error: cannot open file debian/changelog

and shadow-rs.spec pins version 0.2.0 against a 0.2.2 workspace, points
at the pre-transfer repository URL, and lists a %files layout that
'make install' does not produce -- it names the multicall binary, which
only install-multicall builds, and puts every tool in sbin, which is no
longer where half of them go.

Shipping packaging that claims to work and does not is worse than
shipping none, and this would have produced a package declaring
'Provides: passwd, login' on a system whose real passwd it replaced.
Releases are the archives dist publishes; a distribution packager writes
these against their own policy anyway.

* release: publish an aarch64 archive

The pipeline shipped x86_64 only. arm64 servers and Apple-silicon
containers are common enough that a missing archive is a real gap.

It builds on a native arm64 runner rather than cross-compiling, which is
what makes it straightforward: shadow-core::crypt links crypt(3) and the
pam feature links libpam, so a cross build would need an arm64 sysroot
carrying both, while a native runner installs them from apt like any
other build. dist has picked a native runner for aarch64 since 0.30.

Both runners are pinned to 24.04. dist's defaults are ubuntu-22.04 and
ubuntu-22.04-arm, and 22.04 enters deprecation on 2026-09-17 with the
image unsupported from 2027-04-17; the pin keeps the two archives on the
same generation and off an image that is about to start failing. The
higher runner raises the glibc floor for the dynamically linked
archives, which is what the static musl archive is there for.

release.yml is regenerated by 'dist generate'; 'dist plan' now lists
three archives and resolves ubuntu-24.04-arm for the arm64 job.

Closes #222.
2026-09-04 16:28:57 +02:00
Pierre Warnier 202acc16c9 README: rewrite following uutils format, add local Docker git hooks
README.md: project overview with CVE motivation, goals, tool status
table, Docker build/test/lint instructions, architecture diagram,
test matrix, contributing guidelines with GPL clean-room warning.

hooks/: local CI via Docker (no GitHub Actions, no cloud):
- pre-commit: cargo fmt + clippy on Debian (~5s)
- pre-push: fmt + clippy + tests on Debian/Alpine/Fedora (~30s)
- install.sh: symlinks hooks into .git/hooks/
2026-03-23 13:13:36 +01:00