# BST-TIMEOUT: 300 # MEASURED: elapsed 67 s for 85 cases at a 3596 MiB peak # # (2026-08-19, local functional verify, otherwise idle # # host). THE MARKER IS LOAD-BEARING, not a formality: the # # runner's DEFAULT bound is 60 s, so this suite exceeds it # # outright and a developer running # # `brintos-selftest 87-libpwquality.test` bare would get a # # suite-timeout rather than a result. The ticket gates and # # the nightly pass 300 explicitly; this marker is what # # everyone else gets. # # # # The cost is process starts, not libpwquality's work: # # pwscore, pwmake and the probe are three ~4 MB wasm # # modules and this file invokes them about thirty times # # between them. 300 leaves room for a loaded host without # # being so wide that a genuine hang looks like slowness. . "$BST_LIB" # libpwquality 1.4.5 -- password-quality checking on top of cracklib # (BRINTOS-972), meta-oe recipes-extended. What ships is /usr/bin/pwscore, # /usr/bin/pwmake, /etc/security/pwquality.conf, /usr/lib/libpwquality.a, # /usr/include/pwquality.h, /usr/lib/pkgconfig/pwquality.pc and the API probe # /usr/libexec/libpwquality-tests/libpwquality-probe. # # --------------------------------------------------------------------------- # THREE LAYERS, AND THE SUITE COVERS THEM SEPARATELY ON PURPOSE. # # 1. THE RULE CHECKS are libpwquality's own: length, character classes, # credits, repeats, sequences, palindromes, similarity to an old password, # the username check. They run BEFORE cracklib is consulted # (pwquality_check() in src/check.c calls password_check() first and # returns on any negative), so their verdicts are the same here as on any # other platform and every expected value below was taken from a NATIVE # x86-64 build of the same 1.4.5 source. # # 2. THE DICTIONARY CHECK is cracklib's, reached only by a password that # survives layer 1. This is the one layer the native control cannot # measure -- that build has no crack.h, so PWQ_DEFAULT_DICT_CHECK is 0 # there and 1 here -- which makes `pwscore-dictionary` below the case that # proves libpwquality was actually built against cracklib rather than # silently without it. 86-cracklib.test independently establishes that the # compiled dictionary works and that "password" is flagged by it; this # file asserts that libpwquality ROUTES to it. # # 3. THE SCORE is computed only for a password that passes both. It is a pure # function of the string and min_length, so the exact values below are # deterministic and are asserted as exact -- confirmed by repeat runs on # the native control. # # THE TWO STRONG PASSWORDS ARE NOT CHOSEN FREELY. `Tr0ub4dor&3xample!` and # `Qu3rtyZ!pass` are the two 86-cracklib.test already proves cracklib answers # OK for. Any other "obviously strong" string would risk a cracklib refusal # this file would then read as a scoring regression -- the failure mode being # that a red here would point at libpwquality when the dictionary was what # spoke. # # GENERATED PASSWORDS ARE BOUNDED, NEVER PINNED. pwmake consumes entropy in # 15-bit (3-char) or 9-bit (2-char) rounds chosen bit by bit, so the output # length VARIES for a fixed entropy request -- measured over 40 native runs per # size: 56 bits -> 12-13 chars, 64 -> 13-15, 128 -> 26-28, 256 -> 53-55. The # bounds below are deliberately wider than those observations. The score of a # generated password varies for the same reason (56 bits ranged 78-100 across # 20 runs), so it is bounded too. A file that pinned either would be a # flaky test, not a strict one. # # THE PYTHON BINDINGS ARE ASSERTED ABSENT, from the rootfs, because disabling # them is the port's one substantive recipe change and its failure mode is # silent: a python extension module cannot link on this arm, so if the bindings # ever came back the build would break -- but a stray site-packages directory # from a half-disabled build would not, and would drag an unsatisfiable # python3-core runtime dependency into the image. PROBE=/usr/libexec/libpwquality-tests/libpwquality-probe require pwscore libpwquality/pwscore-present require pwmake libpwquality/pwmake-present # --- a sacrificial first case ------------------------------------------------ # THIS CASE EXISTS TO BE LOST. tools/brintos_local_verify.py reproducibly drops # a suite's FIRST result line before the host parser ever sees it -- observed on # all four runs of BRINTOS-970 (libdbi) and again on the first run of this file, # where the line lost was `libpwquality/staticlib`. It is filed as BRINTOS-1149 # and is a defect in our own host-side tooling, not in the runner or in any # package. # # The harness reports the loss (`declared N case(s), N-1 reached the console`) # but computes its pass count as total-minus-failures, so a dropped line is # counted as a PASS. That is the risk 1149 names exactly: a suite whose first # case is a genuine `not ok` loses the one line that mattered, silently. Putting # a case here that asserts something already true costs one line of output and # means the position that gets eaten never carries an assertion anyone depends # on. It does not hide the defect -- the harness still reports the deficit and # the design doc records it -- it just stops the defect from choosing which of # this file's assertions to discard. # # Remove this case when 1149 is fixed; a suite that needs a decoy is not a # suite anyone should have to explain twice. bst_ok libpwquality/suite-canary # --- the package is whole ---------------------------------------------------- check libpwquality/staticlib test -s /usr/lib/libpwquality.a check libpwquality/header test -s /usr/include/pwquality.h check libpwquality/pkgconfig test -s /usr/lib/pkgconfig/pwquality.pc check libpwquality/conf test -s /etc/security/pwquality.conf # A real ar archive carrying the API. An archive that is present but truncated # would pass the size check above and fail every consumer. check_eq libpwquality/staticlib-ar '!' "$(head -c 7 /usr/lib/libpwquality.a)" if grep -q pwquality_check /usr/lib/libpwquality.a 2>/dev/null; then bst_ok libpwquality/staticlib-symbols else bst_notok libpwquality/staticlib-symbols "libpwquality.a carries no pwquality_check" fi check_out libpwquality/pc-version 'Version: 1.4.5' cat /usr/lib/pkgconfig/pwquality.pc check_out libpwquality/pc-libs '-lpwquality' cat /usr/lib/pkgconfig/pwquality.pc # The PAM module is not built (no libpam in this image) and must not appear. # Its absence is the state the image recipe documents; a file here would mean # configure found a libpam nobody installed. check_eq libpwquality/no-pam-module '' "$(ls /lib/security/pam_pwquality.so /usr/lib/security/pam_pwquality.so 2>/dev/null)" # The python bindings, asserted absent -- see the header. check_eq libpwquality/no-python-bindings '' \ "$(find /usr/lib /usr/lib32 -name 'pwquality*.so' -o -name 'pwquality.py' 2>/dev/null)" # --- pwscore: the rule checks (layer 1) -------------------------------------- # Every rc/score here was measured on the native control; these verdicts are # reached before cracklib is consulted, so the two builds must agree. # Shorter than min_length (8). rc=1 is "the check failed", distinct from the # rc=3/rc=4 usage failures asserted further down. check_rc libpwquality/pwscore-short-rc 1 sh -c 'printf "short\n" | pwscore' check_out libpwquality/pwscore-short-msg 'shorter than 8 characters' \ sh -c 'printf "short\n" | pwscore 2>&1; true' # A palindrome is refused by the rule layer even though it is long enough, so # this case cannot be confused with a dictionary hit. check_rc libpwquality/pwscore-palindrome-rc 1 sh -c 'printf "aaaaaaaaaaa\n" | pwscore' check_out libpwquality/pwscore-palindrome-msg 'palindrome' \ sh -c 'printf "aaaaaaaaaaa\n" | pwscore 2>&1; true' # --- pwscore: the dictionary check (layer 2) --------------------------------- # THE CASE THAT PROVES CRACKLIB IS WIRED IN. "password" is 8 characters, so it # passes the length rule and every other rule check, and only the dictionary # can refuse it. On a libpwquality built without crack.h this scores 15 and # passes -- which is exactly what the native control does. check_rc libpwquality/pwscore-dictionary-rc 1 sh -c 'printf "password\n" | pwscore' check_out libpwquality/pwscore-dictionary-msg 'dictionary' \ sh -c 'printf "password\n" | pwscore 2>&1; true' # --- pwscore: the score (layer 3) -------------------------------------------- # Exact values, deterministic for a fixed string. Both passwords are ones # 86-cracklib.test proves cracklib accepts, so a red here is libpwquality's # scoring rather than the dictionary's opinion. check_eq libpwquality/pwscore-strong-100 '100' \ "$(printf 'Tr0ub4dor&3xample!\n' | pwscore 2>&1)" check_eq libpwquality/pwscore-strong-90 '90' \ "$(printf 'Qu3rtyZ!pass\n' | pwscore 2>&1)" # --- pwscore: the exit-status contract --------------------------------------- # Four distinct statuses, and a consumer scripting pwscore depends on telling # them apart: 0 scored, 1 refused, 3 usage, 4 nothing to read. check_rc libpwquality/pwscore-usage-rc 3 sh -c 'printf "x\n" | pwscore a b' check_rc libpwquality/pwscore-nostdin-rc 4 sh -c 'pwscore < /dev/null' # An empty LINE is a password of zero length -- refused (rc=1) -- which is not # the same as no input at all (rc=4) above. check_rc libpwquality/pwscore-emptyline-rc 1 sh -c 'printf "\n" | pwscore' # --- pwmake: generation, and therefore /dev/urandom -------------------------- # pwmake reads /dev/urandom directly (src/generate.c) and answers # PWQ_ERROR_RNG if it cannot open it. 76-rng-tools.test asserts that device is # readable on this image; this is the case that shows libpwquality gets to use # it. check_rc libpwquality/pwmake-rc 0 sh -c 'pwmake 64 > /dev/null' _pw56=$(pwmake 56 2>/dev/null) _pw128=$(pwmake 128 2>/dev/null) _pw256=$(pwmake 256 2>/dev/null) # Bounded, not pinned -- see the header for the measured distributions. _len() { printf '%s' "$1" | wc -c | tr -d ' '; } _between() { # if [ "$4" -ge "$2" ] 2>/dev/null && [ "$4" -le "$3" ] 2>/dev/null; then bst_ok "$1" else bst_notok "$1" "want $2..$3 got:'$4'"; fi } _between libpwquality/pwmake-len-56 11 15 "$(_len "$_pw56")" _between libpwquality/pwmake-len-128 24 31 "$(_len "$_pw128")" _between libpwquality/pwmake-len-256 50 59 "$(_len "$_pw256")" # Successive generations differ. A constant here is the one failure mode of a # password generator that nothing else detects: every individual password still # looks perfectly random. if [ -n "$_pw128" ] && [ "$_pw128" != "$_pw256" ] && [ "$_pw128" != "$(pwmake 128 2>/dev/null)" ]; then bst_ok libpwquality/pwmake-distinct else bst_notok libpwquality/pwmake-distinct "two 128-bit generations were identical" fi # Entropy outside PWQ_MIN/MAX_ENTROPY_BITS (56..256) is CLAMPED with a warning # on stderr, not refused -- so a caller asking for 40 still gets a password. check_rc libpwquality/pwmake-clamp-rc 0 sh -c 'pwmake 40 > /dev/null 2>&1' check_out libpwquality/pwmake-clamp-msg 'outside of the allowed entropy range' \ sh -c 'pwmake 40 2>&1 >/dev/null; true' # Usage failures, distinguished from each other as pwscore's are. check_rc libpwquality/pwmake-noargs-rc 3 sh -c 'pwmake' check_rc libpwquality/pwmake-nonnum-rc 4 sh -c 'pwmake abc' # --- the two front ends agree --------------------------------------------- -- # THE ROUND TRIP THE TICKET ASKS FOR: what pwmake generates, pwscore accepts. # Scored rather than merely accepted, and bounded rather than pinned (a 56-bit # generation scored 78-100 across 20 native runs). _gen=$(pwmake 128 2>/dev/null) _gscore=$(printf '%s\n' "$_gen" | pwscore 2>&1); _grc=$? check_eq libpwquality/roundtrip-rc 0 "$_grc" if [ "$_grc" -eq 0 ] && [ "$_gscore" -ge 50 ] 2>/dev/null; then bst_ok libpwquality/roundtrip-score else bst_notok libpwquality/roundtrip-score "pwmake 128 output scored '$_gscore' (rc=$_grc)" fi # --- the API underneath, through the probe ----------------------------------- check libpwquality/probe-shipped test -s "$PROBE" if [ -x "$PROBE" ]; then out=$("$PROBE" /tmp 2>&1); prc=$? bst_ok libpwquality/probe-exec else out=''; prc=127 bst_notok libpwquality/probe-exec "$PROBE is not executable" fi # The probe's exit status counts ITS OWN failures -- a fixture file it could # not write -- not libpwquality's answers, which are scored case by case below. check_eq libpwquality/probe-rc 0 "$prc" check_eq libpwquality/probe-internal-failures 'PW-internal-failures 0' \ "$(printf '%s\n' "$out" | grep '^PW-internal-failures ')" # One sed per case rather than one per field: the guest is slow and the fork # budget is what decides whether this file finishes inside its bound. pw() { printf '%s\n' "$out" | sed -n "s/^PW-$1 //p"; } check_eq libpwquality/api-settings-alloc 'nonnull' "$(pw default-settings)" # --- the compiled-in defaults ------------------------------------------------ # The baseline every pwscore run on this image starts from, with no config file # read. All measured on the native control EXCEPT dict-check, which is the # arm-independent but BUILD-dependent one below. check_eq libpwquality/api-def-min-length 'rc=0 v=8' "$(pw def-min-length)" check_eq libpwquality/api-def-diff-ok 'rc=0 v=1' "$(pw def-diff-ok)" check_eq libpwquality/api-def-dig-credit 'rc=0 v=0' "$(pw def-dig-credit)" check_eq libpwquality/api-def-up-credit 'rc=0 v=0' "$(pw def-up-credit)" check_eq libpwquality/api-def-low-credit 'rc=0 v=0' "$(pw def-low-credit)" check_eq libpwquality/api-def-oth-credit 'rc=0 v=0' "$(pw def-oth-credit)" check_eq libpwquality/api-def-min-class 'rc=0 v=0' "$(pw def-min-class)" check_eq libpwquality/api-def-max-repeat 'rc=0 v=0' "$(pw def-max-repeat)" check_eq libpwquality/api-def-max-sequence 'rc=0 v=0' "$(pw def-max-sequence)" check_eq libpwquality/api-def-user-check 'rc=0 v=1' "$(pw def-user-check)" check_eq libpwquality/api-def-enforcing 'rc=0 v=1' "$(pw def-enforcing)" check_eq libpwquality/api-def-retry-times 'rc=0 v=1' "$(pw def-retry-times)" # THE BUILD-DEPENDENT DEFAULT, and the second half of the cracklib wiring # assertion. PWQ_DEFAULT_DICT_CHECK is 1 under HAVE_CRACK_H and 0 without it, # so a 0 here means the dictionary check is silently not happening however well # cracklib itself works -- and pwscore-dictionary above would be the only other # thing to notice. The native control reads 0; this is the one default whose # reference is NOT that build. check_eq libpwquality/api-def-dict-check 'rc=0 v=1' "$(pw def-dict-check)" # THE THIRD CRACKLIB-WIRING ASSERTION, and the one whose value the native # control gets wrong for an instructive reason. The dict_path FIELD is unset by # default, and pwquality_check() passes that raw NULL to FascistCheck(), which # then falls back to cracklib's own compiled-in dictionary -- which is why the # port wires no dictionary path anywhere. But the GETTER does not report the # field: under HAVE_CRACK_H it substitutes GetDefaultCracklibDict() for a NULL # (src/settings.c), so a caller asking "which dictionary will be used?" is told, # rather than handed a NULL it would have to resolve itself. # # The path it names is the one 86-cracklib.test asserts exists, which ties the # two suites together: if cracklib ever moved its dictionary, this case and that # one would go red together and for the same reason. A '(null)' here is a # libpwquality built without crack.h -- the native control's answer, not this # image's. check_eq libpwquality/api-def-dict-path 'rc=0 v=/usr/share/cracklib/pw_dict' \ "$(pw def-dict-path)" # --- the setters ------------------------------------------------------------- check_eq libpwquality/api-set-min-length 'rc=0 v=12' "$(pw set-min-length)" # min_length is CLAMPED UP to PWQ_BASE_MIN_LENGTH (6): a caller asking for 3 # does not get 3, and a consumer that trusted its own requested value would # mis-report why a password was refused. check_eq libpwquality/api-clamp-min-length 'rc=0 v=6' "$(pw clamp-min-length)" # Unknown setting ids and type mismatches are refused rather than ignored. check_eq libpwquality/api-set-unknown-int 'rc=-6' "$(pw set-unknown-int)" check_eq libpwquality/api-get-unknown-int 'rc=-6 v=-12345' "$(pw get-unknown-int)" check_eq libpwquality/api-set-str-on-int 'rc=-7' "$(pw set-str-on-int)" check_eq libpwquality/api-set-int-on-str 'rc=-6' "$(pw set-int-on-str)" check_eq libpwquality/api-set-bad-words 'rc=0 v=emcraft brintos' "$(pw set-bad-words)" # --- the "name=value" option form -------------------------------------------- check_eq libpwquality/api-option-minlen 'rc=0 v=14' "$(pw option-minlen)" check_eq libpwquality/api-option-unknown 'rc=-5' "$(pw option-unknown)" check_eq libpwquality/api-option-non-int 'rc=-2' "$(pw option-non-int)" # --- the config-file parser -------------------------------------------------- # A NAMED file that is missing is an error (-3). The DEFAULT config file is the # one exception -- ENOENT on it is tolerated, which is what lets pwscore run on # an image shipping no pwquality.conf. check_eq libpwquality/api-cfg-missing 'rc=-3' "$(pw cfg-missing)" # A good file is read and its values take effect. check_eq libpwquality/api-cfg-good 'rc=0 minlen=11' "$(pw cfg-good)" check_eq libpwquality/api-cfg-good-maxrepeat 'v=3' "$(pw cfg-good-maxrepeat)" # A line with no "=" parses as a setting NAME with no value, so it is reported # as an unknown setting (-5) and NOT as a malformed file. The two codes are # different and a consumer showing "malformed" here would be wrong. check_eq libpwquality/api-cfg-no-equals 'rc=-5' "$(pw cfg-no-equals)" check_eq libpwquality/api-cfg-unknown 'rc=-5' "$(pw cfg-unknown)" # PWQ_ERROR_CFGFILE_MALFORMED (-4) has exactly one trigger: a line that does # not fit the parser's 1023-char buffer and is not ended by EOF. check_eq libpwquality/api-cfg-longline 'rc=-4' "$(pw cfg-longline)" # --- the error strings a consumer shows its user ----------------------------- check_eq libpwquality/api-err-min-length 'The password is too short' "$(pw err-min-length)" check_eq libpwquality/api-err-palindrome 'The password is a palindrome' "$(pw err-palindrome)" check_eq libpwquality/api-err-empty 'No password supplied' "$(pw err-empty)" check_eq libpwquality/api-err-rng \ 'Cannot obtain random numbers from the RNG device' "$(pw err-rng)" check_eq libpwquality/api-err-cracklib \ 'The password fails the dictionary check' "$(pw err-cracklib)" # --- check() paths the front ends cannot reach ------------------------------- # pwscore always passes NULL for the old password and a user only from argv, so # these three codes are unreachable from the command line. check_eq libpwquality/api-check-empty 'rc=-20' "$(pw check-empty)" check_eq libpwquality/api-check-null 'rc=-20' "$(pw check-null)" check_eq libpwquality/api-check-same 'rc=-21' "$(pw check-same)" check_eq libpwquality/api-check-user 'rc=-25' "$(pw check-user)" check_eq libpwquality/api-check-short 'rc=-14' "$(pw check-short)" # --- generate() through the API ---------------------------------------------- # Bounded for the same reason pwmake's output is; see the header. _grc56=$(pw gen-56); _grc128=$(pw gen-128) case "$_grc56" in "rc=0 len="*) bst_ok libpwquality/api-gen-56-rc ;; *) bst_notok libpwquality/api-gen-56-rc "want rc=0 got:'$_grc56'" ;; esac case "$_grc128" in "rc=0 len="*) bst_ok libpwquality/api-gen-128-rc ;; *) bst_notok libpwquality/api-gen-128-rc "want rc=0 got:'$_grc128'" ;; esac _between libpwquality/api-gen-56-len 11 15 "${_grc56##*len=}" _between libpwquality/api-gen-128-len 24 31 "${_grc128##*len=}" # A generated password must pass the very checks that generated it -- pwmake # retries up to PWQ_NUM_GENERATION_TRIES precisely to guarantee this. _s56=$(pw gen-56-score); _s128=$(pw gen-128-score) if [ "$_s56" -ge 40 ] 2>/dev/null; then bst_ok libpwquality/api-gen-56-score else bst_notok libpwquality/api-gen-56-score "56-bit generation scored '$_s56'"; fi if [ "$_s128" -ge 90 ] 2>/dev/null; then bst_ok libpwquality/api-gen-128-score else bst_notok libpwquality/api-gen-128-score "128-bit generation scored '$_s128'"; fi # Two successive API generations differ -- the entropy source is live. check_eq libpwquality/api-gen-distinct 'differ' "$(pw gen-distinct)"