challenger

OAuth 2.0-based authentication service that validates user can receive messages at a certain address
Log | Files | Refs | Submodules | README | LICENSE

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:
Asrc/challenger/test-challenger-pinlimit.conf | 18++++++++++++++++++
Asrc/challenger/test-challenger-pinlimit.sh | 226+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
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