#!/bin/sh
# Verify the claims debian-13-passphrase-hardening makes about its components in
# combination, on a Debian 13 (trixie) host with all of them applied.
#
# Each component verifies its own claims. This checks only what holds for the
# combination and for no component alone, through real password changes on a
# throwaway account, so it requires --with-test-account:
#
#   ism-1558  word count (debian-13-pam-wordcount) together with sequence, repeat
#             and dictionary rules (debian-13-pwquality-predictability), through one stack
#   ism-2080  no complexity requirement imposed by the stack as a whole: the
#             word-count check leaves single tokens alone
#
# Exit status: 0 if nothing FAILs, 1 otherwise.

set -u

[ "${1:-}" = "--with-test-account" ] || {
	echo "error: these claims can only be tested through real password changes; pass --with-test-account" >&2
	exit 2
}

PASS_COUNT=0
FAIL_COUNT=0
PARTIAL_COUNT=0

report() {
	case "$1" in
		PASS) PASS_COUNT=$((PASS_COUNT + 1)) ;;
		PARTIAL) PARTIAL_COUNT=$((PARTIAL_COUNT + 1)) ;;
		*) FAIL_COUNT=$((FAIL_COUNT + 1)) ;;
	esac
	printf '%-8s %-13s %s\n' "$1" "$2" "$3"
}

MIN_WORDS=$(sed -n 's/.*pam_ism_wordcount\.so.*minwords=\([0-9]*\).*/\1/p' /etc/pam.d/common-password | head -1)
[ -n "$MIN_WORDS" ] || { [ -r /etc/security/ism-wordcount.conf ] && . /etc/security/ism-wordcount.conf; }
[ -n "${MIN_WORDS:-}" ] || { echo "error: no word-count enforcement found; apply debian-13-pam-wordcount" >&2; exit 1; }

TEST_USER="ismcap$$"
useradd -M -s /usr/sbin/nologin "$TEST_USER" >/dev/null 2>&1 || { echo "error: cannot create a throwaway account" >&2; exit 1; }
trap 'userdel -f "$TEST_USER" >/dev/null 2>&1' EXIT
accepted() { echo "$TEST_USER:$1" | chpasswd >/dev/null 2>&1; }
verdict() { accepted "$1" && echo accepted || echo rejected; }

# Longer than any applicability's minimum length, so length is never the reason.
single_token=vaultottermangoquiltravenfjordzebraplinth
short_seq=''
i=1
while [ "$i" -lt "$MIN_WORDS" ]; do
	short_seq="${short_seq}otter${i}mango "
	i=$((i + 1))
done
full_seq="${short_seq}quiltraven"
sequence="${single_token}abcdefgh"

# --- ism-2080 : no complexity requirement from the stack as a whole -----------
if accepted "$single_token"; then
	report PASS "ism-2080" "all-lowercase single token accepted through pam_pwquality and the word-count check together"
else
	report FAIL "ism-2080" "all-lowercase single token rejected by the combined stack; a complexity rule is imposed"
fi

# --- ism-1558 : every enforceable limb, through one stack --------------------
SHORT=$(verdict "$short_seq")
FULL=$(verdict "$full_seq")
SEQ=$(verdict "$sequence")
NATURAL=0
for phrase in "the quick brown fox jumps over the lazy dog" \
	"we all live in a yellow submarine yellow submarine"; do
	accepted "$phrase" && NATURAL=$((NATURAL + 1))
done
if [ "$SHORT" = rejected ] && [ "$FULL" = accepted ] && [ "$SEQ" = rejected ]; then
	if [ "$NATURAL" -gt 0 ]; then
		report PARTIAL "ism-1558" \
			"word count and monotonic sequences enforced together; $NATURAL/2 natural-language sentences still accepted"
	else
		report PASS "ism-1558" "word count, sequences and natural-language sentences all rejected"
	fi
else
	report FAIL "ism-1558" \
		"combined stack: $((MIN_WORDS - 1)) words $SHORT, $MIN_WORDS words $FULL, monotonic sequence $SEQ"
fi

echo
echo "passed: $PASS_COUNT  partial: $PARTIAL_COUNT  failed: $FAIL_COUNT"
[ "$FAIL_COUNT" -eq 0 ] || exit 1
