challenger

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

commit b5ccb7ed5411b32b5fb5a0a5963f7ab60a169c0c
parent 2f6ef3d098daab8f0a5a0c64b15ac35f885fc22d
Author: Christian Grothoff <christian@grothoff.org>
Date:   Thu,  6 Aug 2026 14:50:24 +0200

/solve should consult pin_transmissions_left before declaring user out of options

Diffstat:
Msrc/challenger/challenger-httpd_solve.c | 6+++++-
Msrc/challenger/meson.build | 7+++++++
Asrc/challenger/test-challenger-exhaustion.conf | 18++++++++++++++++++
Asrc/challenger/test-challenger-exhaustion.sh | 202+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
4 files changed, 232 insertions(+), 1 deletion(-)

diff --git a/src/challenger/challenger-httpd_solve.c b/src/challenger/challenger-httpd_solve.c @@ -324,8 +324,12 @@ CH_handler_solve (struct CH_HandlerContext *hc, enum MHD_Result ret; json_t *details; + /* Only give up if *no* option remains: a /challenge retransmission + resets auth_attempts_left to 3, so as long as the user may still + request another PIN transmission they can still succeed. */ if ( (0 == bc->addr_left) && - (0 == bc->auth_attempts_left) ) + (0 == bc->auth_attempts_left) && + (0 == bc->pin_transmissions_left) ) { GNUNET_log (GNUNET_ERROR_TYPE_INFO, "Client exhausted all chances to satisfy challenge\n"); diff --git a/src/challenger/meson.build b/src/challenger/meson.build @@ -18,6 +18,7 @@ check_SCRIPTS = [ 'test-challenger-token-errors', 'test-challenger-pinfail', 'test-challenger-resend', + 'test-challenger-exhaustion', ] test_helper_cat = configure_file(input: 'cat.sh', output: 'cat.sh', copy: true) @@ -52,6 +53,12 @@ test_helper_cat_once = configure_file( copy: true, ) +test_conf = configure_file( + input: 'test-challenger-exhaustion.conf', + output: 'test-challenger-exhaustion.conf', + copy: true, +) + foreach s : check_SCRIPTS tscript = '@0@.sh'.format(s) test_exe = configure_file(input: tscript, output: tscript, copy: true) diff --git a/src/challenger/test-challenger-exhaustion.conf b/src/challenger/test-challenger-exhaustion.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/ + +# Short cooldown so that the exhaustion test can exercise a PIN +# retransmission without sleeping for minutes. +PIN_RETRANSMISSION_FREQUENCY = 1 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-exhaustion.sh b/src/challenger/test-challenger-exhaustion.sh @@ -0,0 +1,202 @@ +#!/usr/bin/env bash +# This file is in the public domain. +# +# Regression test for finding 11: '/solve' must not declare the user +# permanently out of options while a PIN retransmission is still available. +# +# A '/challenge' retransmission resets auth_attempts_left to 3, so a +# validation with pin_transmissions_left > 0 can still be completed even +# when both the address budget and the current PIN's guess budget are +# spent. Answering such a request with "users exhausted all possibilities +# of passing the check" is wrong and the SPA presents it as terminal. +# +# The test drives a validation to address_attempts_left = 0, +# auth_attempts_left = 0, pin_transmissions_left > 0, checks that the +# terminal answer is *not* given, and then completes the validation via a +# retransmission to prove that the user really was not out of options. + +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-exhaustion.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-exhaustion.conf" +BURL="http://localhost:9967" +REDIRECT_URI="http://client.example.com/" +CLIENT_SECRET="secret-token:secret" +CLIENT_ID=1 + +echo -n "Initialize challenger database ..." +challenger-dbinit -r -c "${CONF}" &> dbinit.log +echo " OK" + +echo -n "Add challenger client ..." +challenger-admin -c "${CONF}" -a "${CLIENT_SECRET}" "${REDIRECT_URI}" &> admin.log +echo " OK" + +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" + +# Start a validation and get it into the 'address submitted' state. +function new_validation() { + 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 "/setup: expected 200 OK. Got: $STATUS" $(cat $LAST_RESPONSE) + fi + NONCE=$(jq -r .nonce < "$LAST_RESPONSE") + STATUS=$(curl "${BURL}/authorize/${NONCE}" \ + -G \ + -H "Accept: application/json" \ + --data-urlencode "response_type=code" \ + --data-urlencode "client_id=${CLIENT_ID}" \ + --data-urlencode "redirect_uri=${REDIRECT_URI}" \ + -w "%{http_code}" -s -o $LAST_RESPONSE) + if [ "$STATUS" != "200" ] + then + exit_fail "/authorize: expected 200 OK. Got: $STATUS" $(cat $LAST_RESPONSE) + fi +} + +# submit_address $1=address-suffix; leaves the current PIN in $PIN +function submit_address() { + rm -f "${FILENAME}" + STATUS=$(curl "${BURL}/challenge/${NONCE}" \ + -X POST \ + -H "Accept: application/json" \ + --data-urlencode "filename=${FILENAME}" \ + --data-urlencode "note=$1" \ + -w "%{http_code}" -s -o $LAST_RESPONSE) + if [ "$STATUS" != "200" ] + then + exit_fail "/challenge: expected 200 OK. Got: $STATUS" $(cat $LAST_RESPONSE) + fi + # Every submission here transmits: changing the address refills the + # transmission budget and clears the cooldown, and the retransmission + # at the end waits the cooldown out. + PIN=$(awk '{print $5}' < "${FILENAME}") +} + +# solve $1=pin; leaves the status in $STATUS +function solve() { + STATUS=$(curl "${BURL}/solve/${NONCE}" \ + -X POST \ + -H "Accept: application/json" \ + --data-urlencode "pin=$1" \ + -w "%{http_code}" -s -o $LAST_RESPONSE) +} + +# Burn all three guesses for the currently transmitted PIN. +function burn_guesses() { + for i in 1 2 3 + do + solve "$(( (10#${PIN} + i) % 100000000 ))" + if [ "$STATUS" = "200" ] || [ "$STATUS" = "302" ] + then + exit_fail "/solve accepted a wrong PIN!" + fi + done +} + +echo -n "Driving validation to 'no addresses, no guesses, PIN left' ..." +new_validation +# Three address submissions: each one spends an address attempt (of the +# three the validation starts with), transmits a PIN for the new address +# and thus resets auth_attempts_left to 3, which burn_guesses then spends. +# That leaves the validation with no addresses and no guesses, but with +# PIN transmissions still to spare. +for a in one two three +do + submit_address "${a}" + burn_guesses +done +echo " OK" + +echo -n "Wrong PIN must not be answered as terminal exhaustion ..." +solve "$(( (10#${PIN} + 1) % 100000000 ))" +if grep -q "exhausted all possibilities" "$LAST_RESPONSE" +then + exit_fail "/solve claims the user is out of options while a PIN retransmission is still available: $(cat $LAST_RESPONSE)" +fi +echo " OK" + +# Prove the user really was not out of options: retransmit and solve. +echo -n "Retransmitting the PIN ..." +sleep 2 +submit_address "three" +echo " OK" + +echo -n "Solving with the retransmitted PIN ${PIN} ..." +solve "${PIN}" +if [ "$STATUS" != "200" ] +then + exit_fail "/solve: expected 200 OK after retransmission. Got: $STATUS" $(cat $LAST_RESPONSE) +fi +TYPE=$(jq -r .type < "$LAST_RESPONSE") +if [ "$TYPE" != "completed" ] +then + exit_fail "/solve: expected 'completed'. Got: $TYPE" $(cat $LAST_RESPONSE) +fi +echo " OK" + +exit 0