commit 38a83ebd191d19d6c4ffc41023000147758c8483
parent 096cfadd1008089cdbe54d79dc12090a87e83d0c
Author: Christian Grothoff <christian@grothoff.org>
Date: Thu, 6 Aug 2026 09:50:05 +0200
compare normalized JSONB addresses, not text encodings
Diffstat:
2 files changed, 244 insertions(+), 0 deletions(-)
diff --git a/src/challenger/test-challenger-pinlimit.conf b/src/challenger/test-challenger-pinlimit.conf
@@ -0,0 +1,18 @@
+[challenger]
+
+# Which external command should be used to transmit challenges?
+AUTH_COMMAND = cat.sh
+
+# What address type are we validating? (SMS, e-mail, etc.)
+ADDRESS_TYPE = file-access
+
+# Base URL
+BASE_URL = http://localhost/
+
+# No cooldown between (re)transmissions, so that the test can exhaust
+# the PIN transmission budget without sleeping.
+PIN_RETRANSMISSION_FREQUENCY = 0 s
+
+[challengerdb-postgres]
+#The connection string the plugin has to use for connecting to the database
+CONFIG = postgres:///talercheck
diff --git a/src/challenger/test-challenger-pinlimit.sh b/src/challenger/test-challenger-pinlimit.sh
@@ -0,0 +1,226 @@
+#!/usr/bin/env bash
+# This file is in the public domain.
+#
+# Tests the per-validation message budget of a challenge.
+#
+# By design a validation allows a fixed number of *addresses* and, for each
+# of them, a fixed number of PIN transmissions -- 3 x 3 -- so a user who
+# mistyped their address is not made to wait out the cooldown of a message
+# that went to somebody else. The ceiling of nine messages is therefore
+# intended, but it is intended to be spread over three *distinct* addresses.
+#
+# Regression test: do_challenge_address.sql used to decide "is this the same
+# address?" on the raw JSON text. Submitting the very same address with its
+# fields in a different order then counted as a new address, so all nine
+# messages could be aimed at one recipient -- and an honest user could lose
+# an address attempt to nothing but a reordered form submission (the daemon
+# itself re-appends 'read_only' as the last field).
+#
+# Also checks the property that makes the 3 x 3 budget safe: a PIN that was
+# transmitted for one address must not solve the challenge once the address
+# has been changed.
+
+set -eu
+
+# Exit, with status code "skip" (no 'real' failure)
+function exit_skip() {
+ echo " SKIP: $1"
+ exit 77
+}
+
+# Exit, with error message (hard failure)
+function exit_fail() {
+ echo " FAIL: $@"
+ exit 1
+}
+
+# Cleanup to run whenever we exit
+function cleanup()
+{
+ for n in $(jobs -p)
+ do
+ kill $n 2> /dev/null || true
+ done
+ rm -f "$LAST_RESPONSE" "$FILENAME"
+ wait
+}
+
+LAST_RESPONSE=$(mktemp responseXXXXXX.log)
+FILENAME="test-challenger-pinlimit.txt"
+
+# Install cleanup handler (except for kill -9)
+trap cleanup EXIT
+
+export PATH="$PATH:."
+
+echo -n "Testing for jq"
+jq -h > /dev/null || exit_skip "jq required"
+echo " FOUND"
+echo -n "Testing for curl"
+curl -h > /dev/null || exit_skip "curl required"
+echo " FOUND"
+echo -n "Testing for wget"
+wget -h > /dev/null || exit_skip "wget required"
+echo " FOUND"
+echo -n "Testing for challenger-httpd ..."
+challenger-httpd -h > /dev/null || exit_skip "challenger-httpd required"
+echo " FOUND"
+
+CONF="test-challenger-pinlimit.conf"
+BURL="http://localhost:9967"
+REDIRECT_URI="http://client.example.com/"
+
+echo -n "Initialize challenger database ..."
+challenger-dbinit -r -c "${CONF}" &> dbinit.log
+echo " OK"
+
+echo -n "Add challenger client ..."
+CLIENT_SECRET="secret-token:secret"
+challenger-admin -c "${CONF}" -a "${CLIENT_SECRET}" "${REDIRECT_URI}" &> admin.log
+echo " OK"
+# We just reset the DB, thus the client ID must be 1 here:
+CLIENT_ID=1
+
+echo -n "Start challenger-httpd ..."
+challenger-httpd -L INFO -c "${CONF}" &> httpd.log &
+
+# Wait for challenger to be available
+for n in $(seq 1 50)
+do
+ echo -n "."
+ sleep 0.2
+ OK=0
+ wget --tries=1 --timeout=1 "${BURL}/config" -o /dev/null -O /dev/null >/dev/null || continue
+ OK=1
+ break
+done
+if [ 1 != $OK ]
+then
+ exit_skip "Failed to launch challenger service"
+fi
+echo " OK"
+
+echo -n "Setup new validation process..."
+STATUS=$(curl "${BURL}/setup/${CLIENT_ID}" \
+ -H "Authorization: Bearer ${CLIENT_SECRET}" \
+ -d '' \
+ -w "%{http_code}" -s -o $LAST_RESPONSE)
+
+if [ "$STATUS" != "200" ]
+then
+ exit_fail "Expected 200 OK. Got: $STATUS" $(cat $LAST_RESPONSE)
+fi
+NONCE=$(jq -r .nonce < "$LAST_RESPONSE")
+echo " OK"
+
+# All addresses used below deliver to the *same* recipient: cat.sh only
+# looks at the "filename" field, "tag" is decoration that the (empty)
+# ADDRESS_RESTRICTIONS do not constrain.
+SENT=0
+
+# submit /challenge with the given curl arguments; counts transmissions in
+# $SENT, leaves the HTTP status in $STATUS.
+function submit()
+{
+ rm -f "${FILENAME}"
+ STATUS=$(curl "${BURL}/challenge/${NONCE}" \
+ -X POST \
+ -H "Accept: application/json" \
+ "$@" \
+ -w "%{http_code}" -s -o $LAST_RESPONSE)
+ if [ "true" = "$(jq -r '.transmitted // false' < "$LAST_RESPONSE")" ]
+ then
+ SENT=$(( SENT + 1 ))
+ PIN=$(awk '{print $5}' < "${FILENAME}")
+ fi
+}
+
+# submit_address $1=tag: the address {"filename":...,"tag":...}
+function submit_address()
+{
+ submit --data-urlencode "filename=${FILENAME}" \
+ --data-urlencode "tag=$1"
+}
+
+# expect_sent $1=count $2=what was submitted
+function expect_sent()
+{
+ if [ "$SENT" != "$1" ]
+ then
+ exit_fail "expected ${1} PIN transmissions after $2, counted ${SENT}"
+ fi
+}
+
+# expect_status $1=code $2=what was submitted
+function expect_status()
+{
+ if [ "$STATUS" != "$1" ]
+ then
+ exit_fail "/challenge: expected ${1} for $2. Got: $STATUS" \
+ $(cat $LAST_RESPONSE)
+ fi
+}
+
+echo -n "Spending the three PIN transmissions of the first address..."
+for i in 1 2 3
+do
+ submit_address "one"
+ expect_status 200 "a PIN transmission of the first address"
+ echo -n "."
+done
+expect_sent 3 "three submissions of the first address"
+PIN_ONE="${PIN}"
+echo " OK"
+
+echo -n "A fourth transmission for that address must be refused..."
+submit_address "one"
+expect_status 429 "a fourth transmission of the same address"
+expect_sent 3 "a fourth transmission of the same address"
+echo " OK"
+
+# Same JSON value, fields in the other order. This is the regression: with
+# a textual comparison this counts as a new address, refills the budget and
+# transmits a fourth PIN to the very same recipient.
+echo -n "The same address with its fields reordered is not a new address..."
+submit --data-urlencode "tag=one" \
+ --data-urlencode "filename=${FILENAME}"
+expect_status 429 "a reordered rendering of the current address"
+expect_sent 3 "a reordered rendering of the current address"
+echo " OK"
+
+echo -n "Changing the address transmits and invalidates the previous PIN..."
+submit_address "two"
+expect_status 200 "the first submission of the second address"
+expect_sent 4 "the first submission of the second address"
+RESULT=$(curl "${BURL}/solve/${NONCE}" \
+ -X POST \
+ -H "Accept: application/json" \
+ --data-urlencode "pin=${PIN_ONE}" \
+ -w "%{http_code}" -s -o $LAST_RESPONSE)
+if [ "$RESULT" = "302" ] || [ "$RESULT" = "200" ]
+then
+ exit_fail "the PIN sent for the previous address solved the challenge"
+fi
+echo " OK ($RESULT)"
+
+echo -n "Spending the rest of the designed 3 x 3 budget..."
+for i in 2 3
+do
+ submit_address "two"
+ expect_status 200 "a PIN transmission of the second address"
+done
+for i in 1 2 3
+do
+ submit_address "three"
+ expect_status 200 "a PIN transmission of the third address"
+done
+expect_sent 9 "three transmissions for each of three addresses"
+echo " OK (9)"
+
+echo -n "A fourth address must be refused..."
+submit_address "four"
+expect_status 429 "a fourth address"
+expect_sent 9 "a fourth address"
+echo " OK"
+
+exit 0