challenger

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

commit 35f111bdbfb0a80ab398139dc5e4ff66679a4198
parent 4cce9f30d17521bdbf2207ad526c9624f164cd2a
Author: Christian Grothoff <christian@grothoff.org>
Date:   Thu,  6 Aug 2026 23:09:34 +0200

more stricter standards-compliance of various responses

Diffstat:
Msrc/challenger/challenger-httpd.c | 23++++++++++++++++++++++-
Msrc/challenger/challenger-httpd_authorize.c | 37+++++++++++++++++++++++++++++++++++++
Msrc/challenger/challenger-httpd_challenge.c | 17+++++++++++++++--
Msrc/challenger/challenger-httpd_common.c | 52+++++++++++++++++++++++++++++++++++++++++++++++-----
Msrc/challenger/challenger-httpd_common.h | 16+++++++++++++++-
Msrc/challenger/challenger-httpd_config.c | 15++++++++++-----
Msrc/challenger/challenger-httpd_info.c | 27+++++++++++++++++----------
Msrc/challenger/challenger-httpd_solve.c | 18+++++++++---------
Msrc/challenger/challenger-httpd_token.c | 97++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----------
Msrc/challenger/test-challenger-auth-errors.sh | 23+++++++++++++++++++++--
Msrc/challenger/test-challenger-badutf8.sh | 26++++++++++++++++++++++++++
Msrc/challenger/test-challenger-exhaustion.sh | 57+++++++++++++++++++++++++++++++++++++++++++++++++++++++++
12 files changed, 360 insertions(+), 48 deletions(-)

diff --git a/src/challenger/challenger-httpd.c b/src/challenger/challenger-httpd.c @@ -284,6 +284,14 @@ url_handler (void *cls, hc->rh = rh; break; } + if ( (0 == strcasecmp (method, + MHD_HTTP_METHOD_HEAD)) && + (0 == strcasecmp (rh->method, + MHD_HTTP_METHOD_GET)) ) + { + hc->rh = rh; + break; + } } } if (NULL == hc->rh) @@ -297,7 +305,8 @@ url_handler (void *cls, GNUNET_break_op (0); /* Build Allow header: OPTIONS is always supported (handled above), - plus every method registered for this URL. */ + plus every method registered for this URL, plus HEAD wherever + GET is registered */ for (unsigned int j = 0; NULL != handlers[j].url; j++) { const struct CH_RequestHandler *rh2 = &handlers[j]; @@ -313,6 +322,18 @@ url_handler (void *cls, memcpy (&allow[aoff], rh2->method, strlen (rh2->method)); aoff += strlen (rh2->method); allow[aoff] = '\0'; + if (0 != strcasecmp (rh2->method, + MHD_HTTP_METHOD_GET)) + continue; + GNUNET_assert (aoff + strlen (MHD_HTTP_METHOD_HEAD) + 3 + < sizeof (allow)); + memcpy (&allow[aoff], ", ", 2); + aoff += 2; + memcpy (&allow[aoff], + MHD_HTTP_METHOD_HEAD, + strlen (MHD_HTTP_METHOD_HEAD)); + aoff += strlen (MHD_HTTP_METHOD_HEAD); + allow[aoff] = '\0'; } resp = MHD_create_response_from_buffer (0, diff --git a/src/challenger/challenger-httpd_authorize.c b/src/challenger/challenger-httpd_authorize.c @@ -209,6 +209,43 @@ CH_handler_authorize (struct CH_HandlerContext *hc, = MHD_lookup_connection_value (hc->connection, MHD_GET_ARGUMENT_KIND, "scope"); + /* Everything below is stored verbatim in text columns. Postgres rejects + invalid UTF-8 with SQLSTATE 22021, which we would surface as a 500; + catch it here so a bad request is reported as one. */ + { + const struct + { + const char *name; + const char *value; + } texts[] = { + { "redirect_uri", redirect_uri }, + { "state", state }, + { "scope", scope }, + { "code_challenge", code_challenge }, + { NULL, NULL } + }; + + for (unsigned int i = 0; NULL != texts[i].name; i++) + { + json_t *probe; + + if (NULL == texts[i].value) + continue; + /* json_string() returns NULL for anything that is not valid UTF-8; + this is the same check /challenge applies to its form fields. */ + probe = json_string (texts[i].value); + if (NULL == probe) + { + GNUNET_break_op (0); + return reply_error ( + hc, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + texts[i].name); + } + json_decref (probe); + } + } { json_t *last_address = NULL; uint32_t address_attempts_left; diff --git a/src/challenger/challenger-httpd_challenge.c b/src/challenger/challenger-httpd_challenge.c @@ -808,9 +808,9 @@ CH_handler_challenge (struct CH_HandlerContext *hc, GNUNET_break_op (0); return TALER_MHD_reply_with_error ( hc->connection, - MHD_HTTP_BAD_REQUEST, + MHD_HTTP_UNSUPPORTED_MEDIA_TYPE, TALER_EC_GENERIC_PARAMETER_MALFORMED, - "Content-Type"); + MHD_HTTP_HEADER_CONTENT_TYPE); } } GNUNET_log (GNUNET_ERROR_TYPE_INFO, @@ -909,6 +909,19 @@ CH_handler_challenge (struct CH_HandlerContext *hc, TALER_EC_GENERIC_PARAMETER_MALFORMED, bc->bad_key); } + if (! json_is_object (bc->address)) + { + /* The address is echoed back with GNUNET_JSON_pack_object_incref() and + (if a message template is configured) expanded by send_tan(), both of + which assert on anything that is not an object; refuse it here + instead. Only reachable through an "application/json" upload, as the + form encodings always build an object. */ + GNUNET_break_op (0); + return reply_error (bc, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "address"); + } #if DEBUG { char *address; diff --git a/src/challenger/challenger-httpd_common.c b/src/challenger/challenger-httpd_common.c @@ -168,6 +168,29 @@ CH_code_to_nonce (const char *code, } +/** + * Return a copy of @a hint that is safe to embed in the quoted-string of + * an HTTP header: RFC 9110 quoted-strings may not contain a bare '"' or + * '\\', and error code hints are free-form text that sometimes does. + * Rather than emitting quoted-pairs (which sloppy clients mis-parse), the + * offending characters are substituted. + * + * @param hint text to sanitize + * @return newly allocated sanitized string + */ +static char * +quote_safe (const char *hint) +{ + char *out = GNUNET_strdup (hint); + + for (char *p = out; '\0' != *p; p++) + if ( ('"' == *p) || + ('\\' == *p) ) + *p = '\''; + return out; +} + + enum MHD_Result CH_reply_with_oauth_error ( struct MHD_Connection *connection, @@ -182,8 +205,9 @@ CH_reply_with_oauth_error ( resp = TALER_MHD_make_json_steal ( GNUNET_JSON_PACK ( TALER_MHD_PACK_EC (ec), - GNUNET_JSON_pack_string ("error", - oauth_error), + GNUNET_JSON_pack_allow_null ( + GNUNET_JSON_pack_string ("error", + oauth_error)), GNUNET_JSON_pack_allow_null ( GNUNET_JSON_pack_string ("detail", detail)))); @@ -191,9 +215,27 @@ CH_reply_with_oauth_error ( { char *www_auth; - GNUNET_asprintf (&www_auth, - "Bearer error=\"%s\"", - oauth_error); + /* RFC 6750 section 3: the challenge carries a realm, and SHOULD carry + an error_description; but when the request contained no + authentication credentials at all it must NOT carry an error code, + which the caller signals by passing NULL for @a oauth_error. */ + if (NULL == oauth_error) + { + GNUNET_asprintf (&www_auth, + "Bearer realm=\"%s\"", + CH_OAUTH_REALM); + } + else + { + char *desc = quote_safe (TALER_ErrorCode_get_hint (ec)); + + GNUNET_asprintf (&www_auth, + "Bearer realm=\"%s\", error=\"%s\", error_description=\"%s\"", + CH_OAUTH_REALM, + oauth_error, + desc); + GNUNET_free (desc); + } GNUNET_break (MHD_YES == MHD_add_response_header ( resp, diff --git a/src/challenger/challenger-httpd_common.h b/src/challenger/challenger-httpd_common.h @@ -24,6 +24,14 @@ #include "challenger-httpd.h" /** + * Protection space announced in the "WWW-Authenticate" challenge, as per + * RFC 6750 section 3. Challenger serves a single protection space, so + * this is a constant. + */ +#define CH_OAUTH_REALM "challenger" + + +/** * Extract the client secret from the * authorization header of @a connection. * @@ -105,9 +113,15 @@ CH_build_full_redirect_url (const struct CHALLENGER_ValidationNonceP *nonce, * Send a OAuth 2.0 response indicating an error following * section 5.2 of RFC 6749. * + * On a 401 this also emits the "WWW-Authenticate" challenge required by + * RFC 6750 section 3, carrying the realm and (unless @a oauth_error is + * NULL) the error code and a human-readable error_description. + * * @param connection the MHD connection to use * @param ec error code uniquely identifying the error - * @param oauth_error error as of the OAuth 2.0 protocol + * @param oauth_error error as of the OAuth 2.0 protocol, or NULL if the + * request carried no authentication credentials at all: RFC 6750 + * section 3 requires that the challenge then omits the error code * @param http_status HTTP status code to use * @param detail additional optional detail about the error * @return a MHD result code diff --git a/src/challenger/challenger-httpd_config.c b/src/challenger/challenger-httpd_config.c @@ -36,11 +36,16 @@ * 7: added ``build_version`` field in ``/config`` * 8: HTTP status code and error code corrections: /token no longer * returns 409 and /info no longer returns 404; /solve tells a wrong - * PIN from one that could not be checked at all; /challenge and /solve - * use distinct error codes for each way of running out of attempts; - * address restrictions have their own error codes and a restriction we - * cannot evaluate is a 500 rather than a 400; a malformed nonce is a - * 400 everywhere; /setup answers missing credentials with 403. + * PIN from one that could not be checked at all and answers every + * failure with the same body shape; /challenge and /solve use distinct + * error codes for each way of running out of attempts; address + * restrictions have their own error codes and a restriction we cannot + * evaluate is a 500 rather than a 400; a malformed nonce is a 400 + * everywhere; /setup answers missing credentials with 403; every error + * code maps to exactly one HTTP status; HEAD is accepted wherever GET + * is; an unusable Content-Type gives 415 rather than 400; and /token + * and /info always carry the RFC 6749 "error" member plus a full + * RFC 6750 "WWW-Authenticate" challenge. */ diff --git a/src/challenger/challenger-httpd_info.c b/src/challenger/challenger-httpd_info.c @@ -78,10 +78,12 @@ CH_handler_info (struct CH_HandlerContext *hc, if (NULL == auth) { GNUNET_break_op (0); + /* RFC 6750 section 3: a challenge answering a request that carried no + authentication credentials must not include an error code. */ return CH_reply_with_oauth_error ( hc->connection, MHD_HTTP_UNAUTHORIZED, - "invalid_request", + NULL, TALER_EC_GENERIC_PARAMETER_MISSING, MHD_HTTP_HEADER_AUTHORIZATION); } @@ -99,7 +101,8 @@ CH_handler_info (struct CH_HandlerContext *hc, MHD_HTTP_HEADER_AUTHORIZATION); } token = auth + strlen (BEARER_PREFIX); - + /* RFC 7235: the scheme and the credentials are separated by 1*SP; + note that the single space was already in our BEARER_PREFIX */ if (GNUNET_OK != GNUNET_STRINGS_string_to_data (token, strlen (token), @@ -127,18 +130,22 @@ CH_handler_info (struct CH_HandlerContext *hc, { case GNUNET_DB_STATUS_HARD_ERROR: GNUNET_break (0); - return TALER_MHD_reply_with_error (hc->connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_FETCH_FAILED, - "get_token"); + return CH_reply_with_oauth_error (hc->connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + "server_error", + TALER_EC_GENERIC_DB_FETCH_FAILED, + "get_token"); case GNUNET_DB_STATUS_SOFT_ERROR: if (r < MAX_RETRIES - 1) continue; + /* Retries exhausted on serialization failures; unlike a hard error + this is worth retrying from the client side. */ GNUNET_break (0); - return TALER_MHD_reply_with_error (hc->connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_FETCH_FAILED, - "get_token"); + return CH_reply_with_oauth_error (hc->connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + "server_error", + TALER_EC_GENERIC_DB_SOFT_FAILURE, + "get_token"); case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: return reply_invalid_token (hc->connection); case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: diff --git a/src/challenger/challenger-httpd_solve.c b/src/challenger/challenger-httpd_solve.c @@ -225,9 +225,9 @@ CH_handler_solve (struct CH_HandlerContext *hc, GNUNET_break_op (0); return TALER_MHD_reply_with_error ( hc->connection, - MHD_HTTP_BAD_REQUEST, + MHD_HTTP_UNSUPPORTED_MEDIA_TYPE, TALER_EC_GENERIC_PARAMETER_MALFORMED, - "Content-Type"); + MHD_HTTP_HEADER_CONTENT_TYPE); } return MHD_YES; } @@ -339,15 +339,15 @@ CH_handler_solve (struct CH_HandlerContext *hc, (0 == bc->auth_attempts_left) && (0 == bc->pin_transmissions_left) ) { + /* Terminal: no address change, no retransmission and no guess + remains. Reported with the same body as every other /solve + failure (all counters are zero) so that a client only ever has + to parse one shape; the error code tells the cases apart. */ GNUNET_log (GNUNET_ERROR_TYPE_INFO, "Client exhausted all chances to satisfy challenge\n"); - return TALER_MHD_reply_with_error ( - hc->connection, - MHD_HTTP_TOO_MANY_REQUESTS, - TALER_EC_CHALLENGER_TOO_MANY_ATTEMPTS, - "users exhausted all possibilities of passing the check"); + http_status = MHD_HTTP_TOO_MANY_REQUESTS; + ec = TALER_EC_CHALLENGER_TOO_MANY_ATTEMPTS; } - /* Distinguish the three ways in which a /solve can fail. Only the last one is actually about the PIN that was submitted; reporting the other two as "the PIN code provided is incorrect" misleads @@ -355,7 +355,7 @@ CH_handler_solve (struct CH_HandlerContext *hc, tell "provide your address first" from "wait or ask for a new PIN" from "that PIN was wrong" without parsing the human-readable hint. */ - if (no_challenge) + else if (no_challenge) { /* No PIN was ever transmitted for this validation, so there is nothing to check; the user must POST /challenge first. */ diff --git a/src/challenger/challenger-httpd_token.c b/src/challenger/challenger-httpd_token.c @@ -222,6 +222,60 @@ post_iter (void *cls, } +/** + * Check the "Content-Length" header (if any) of @a connection against + * @a max_len. Same contract as TALER_MHD_check_content_length(), except + * that the error responses keep the "error" member required by RFC 6749 + * section 5.2, which every other error of this endpoint carries. + * + * @param connection connection to check + * @param max_len largest permitted body length + * @param[out] mret set to the result to return if we answered the request + * @return true if a response was queued and the caller must return @a mret + */ +static bool +check_content_length (struct MHD_Connection *connection, + unsigned long long max_len, + enum MHD_Result *mret) +{ + const char *cl; + unsigned long long cv; + char dummy; + + cl = MHD_lookup_connection_value (connection, + MHD_HEADER_KIND, + MHD_HTTP_HEADER_CONTENT_LENGTH); + if (NULL == cl) + return false; /* chunked upload; the per-field cap catches oversize */ + if (1 != sscanf (cl, + "%llu%c", + &cv, + &dummy)) + { + GNUNET_break_op (0); + *mret = CH_reply_with_oauth_error ( + connection, + MHD_HTTP_BAD_REQUEST, + "invalid_request", + TALER_EC_GENERIC_PARAMETER_MALFORMED, + MHD_HTTP_HEADER_CONTENT_LENGTH); + return true; + } + if (cv > max_len) + { + GNUNET_break_op (0); + *mret = CH_reply_with_oauth_error ( + connection, + MHD_HTTP_CONTENT_TOO_LARGE, + "invalid_request", + TALER_EC_GENERIC_UPLOAD_EXCEEDS_LIMIT, + NULL); + return true; + } + return false; +} + + enum MHD_Result CH_handler_token (struct CH_HandlerContext *hc, const char *upload_data, @@ -244,13 +298,19 @@ CH_handler_token (struct CH_HandlerContext *hc, GNUNET_break_op (0); return CH_reply_with_oauth_error ( hc->connection, - MHD_HTTP_BAD_REQUEST, + MHD_HTTP_UNSUPPORTED_MEDIA_TYPE, "invalid_request", TALER_EC_GENERIC_PARAMETER_MALFORMED, - "Content-Type"); + MHD_HTTP_HEADER_CONTENT_TYPE); + } + { + enum MHD_Result mret; + + if (check_content_length (hc->connection, + 2 * 1024, + &mret)) + return mret; } - TALER_MHD_check_content_length (hc->connection, - 2 * 1024); return MHD_YES; } /* handle upload */ @@ -269,9 +329,10 @@ CH_handler_token (struct CH_HandlerContext *hc, if (bc->too_big) { GNUNET_break_op (0); - return TALER_MHD_reply_with_error ( + return CH_reply_with_oauth_error ( hc->connection, MHD_HTTP_CONTENT_TOO_LARGE, + "invalid_request", TALER_EC_GENERIC_UPLOAD_EXCEEDS_LIMIT, NULL); } @@ -371,9 +432,10 @@ CH_handler_token (struct CH_HandlerContext *hc, { case GNUNET_DB_STATUS_HARD_ERROR: GNUNET_break (0); - return TALER_MHD_reply_with_error ( + return CH_reply_with_oauth_error ( hc->connection, MHD_HTTP_INTERNAL_SERVER_ERROR, + "server_error", TALER_EC_GENERIC_DB_FETCH_FAILED, "get_client"); case GNUNET_DB_STATUS_SOFT_ERROR: @@ -394,9 +456,10 @@ CH_handler_token (struct CH_HandlerContext *hc, if (GNUNET_DB_STATUS_SOFT_ERROR == qs) { GNUNET_break (0); - return TALER_MHD_reply_with_error ( + return CH_reply_with_oauth_error ( hc->connection, MHD_HTTP_INTERNAL_SERVER_ERROR, + "server_error", TALER_EC_GENERIC_DB_SOFT_FAILURE, "get_client"); } @@ -405,6 +468,7 @@ CH_handler_token (struct CH_HandlerContext *hc, bc->redirect_uri)) ) { GNUNET_break_op (0); + GNUNET_free (client_url); return CH_reply_with_oauth_error ( hc->connection, MHD_HTTP_UNAUTHORIZED, @@ -466,9 +530,10 @@ CH_handler_token (struct CH_HandlerContext *hc, { case GNUNET_DB_STATUS_HARD_ERROR: GNUNET_break (0); - return TALER_MHD_reply_with_error ( + return CH_reply_with_oauth_error ( hc->connection, MHD_HTTP_INTERNAL_SERVER_ERROR, + "server_error", TALER_EC_GENERIC_DB_FETCH_FAILED, "get_validation_pkce"); case GNUNET_DB_STATUS_SOFT_ERROR: @@ -494,9 +559,10 @@ CH_handler_token (struct CH_HandlerContext *hc, if (GNUNET_DB_STATUS_SOFT_ERROR == qs) { GNUNET_break (0); - return TALER_MHD_reply_with_error ( + return CH_reply_with_oauth_error ( hc->connection, MHD_HTTP_INTERNAL_SERVER_ERROR, + "server_error", TALER_EC_GENERIC_DB_SOFT_FAILURE, "get_validation_pkce"); } @@ -512,10 +578,13 @@ CH_handler_token (struct CH_HandlerContext *hc, GNUNET_free (client_redirect_uri); GNUNET_free (client_state); GNUNET_free (code_challenge); - return TALER_MHD_reply_with_error ( + /* The value comes out of our own database, so a bad one is corrupt + server state, not a malformed request parameter. */ + return CH_reply_with_oauth_error ( hc->connection, MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_PARAMETER_MALFORMED, + "server_error", + TALER_EC_GENERIC_DB_INVARIANT_FAILURE, "Invalid code_challenge_method"); } @@ -744,9 +813,10 @@ CH_handler_token (struct CH_HandlerContext *hc, { case GNUNET_DB_STATUS_HARD_ERROR: GNUNET_break (0); - return TALER_MHD_reply_with_error ( + return CH_reply_with_oauth_error ( hc->connection, MHD_HTTP_INTERNAL_SERVER_ERROR, + "server_error", TALER_EC_GENERIC_DB_STORE_FAILED, "do_insert_token"); case GNUNET_DB_STATUS_SOFT_ERROR: @@ -767,9 +837,10 @@ CH_handler_token (struct CH_HandlerContext *hc, if (GNUNET_DB_STATUS_SOFT_ERROR == qs) { GNUNET_break (0); - return TALER_MHD_reply_with_error ( + return CH_reply_with_oauth_error ( hc->connection, MHD_HTTP_INTERNAL_SERVER_ERROR, + "server_error", TALER_EC_GENERIC_DB_SOFT_FAILURE, "do_insert_token"); } diff --git a/src/challenger/test-challenger-auth-errors.sh b/src/challenger/test-challenger-auth-errors.sh @@ -95,18 +95,30 @@ fi echo " OK" # Check that the last response carried a 'WWW-Authenticate: Bearer' header -# naming the given OAuth error. +# naming the given OAuth error, with the realm and a description as +# required/recommended by RFC 6750 section 3. function check_www_authenticate() { if ! grep -qi "^WWW-Authenticate:" "$LAST_HEADERS" then exit_fail "missing WWW-Authenticate header: $(cat $LAST_HEADERS)" fi + if ! grep -qi "^WWW-Authenticate:.*Bearer.*realm=\"challenger\"" "$LAST_HEADERS" + then + exit_fail "expected WWW-Authenticate to carry realm=\"challenger\". Got: $(grep -i '^WWW-Authenticate:' $LAST_HEADERS)" + fi if ! grep -qi "^WWW-Authenticate:.*Bearer.*error=\"$1\"" "$LAST_HEADERS" then exit_fail "expected WWW-Authenticate 'Bearer error=\"$1\"'. Got: $(grep -i '^WWW-Authenticate:' $LAST_HEADERS)" fi + if ! grep -qi "^WWW-Authenticate:.*error_description=\"" "$LAST_HEADERS" + then + exit_fail "expected WWW-Authenticate to carry an error_description. Got: $(grep -i '^WWW-Authenticate:' $LAST_HEADERS)" + fi } +# RFC 6750 section 3: "If the request lacks any authentication information +# [...] the resource server SHOULD NOT include an error code." So this +# challenge must carry the realm and nothing else. echo -n "/info without an Authorization header ..." STATUS=$(curl "${BURL}/info" \ -D "$LAST_HEADERS" \ @@ -115,7 +127,14 @@ if [ "$STATUS" != "401" ] then exit_fail "Expected 401 Unauthorized. Got: $STATUS" $(cat $LAST_RESPONSE) fi -check_www_authenticate "invalid_request" +if ! grep -qi "^WWW-Authenticate:.*Bearer.*realm=\"challenger\"" "$LAST_HEADERS" +then + exit_fail "expected WWW-Authenticate 'Bearer realm=\"challenger\"'. Got: $(grep -i '^WWW-Authenticate:' $LAST_HEADERS)" +fi +if grep -qi "^WWW-Authenticate:.*error=" "$LAST_HEADERS" +then + exit_fail "challenge for a request without credentials must not name an error code. Got: $(grep -i '^WWW-Authenticate:' $LAST_HEADERS)" +fi echo " OK" echo -n "/info with a non-Bearer Authorization header ..." diff --git a/src/challenger/test-challenger-badutf8.sh b/src/challenger/test-challenger-badutf8.sh @@ -141,6 +141,32 @@ wget --tries=1 --timeout=5 "${BURL}/config" -o /dev/null -O /dev/null \ || exit_fail "challenger-httpd died on a non-UTF-8 field name" echo " OK" +# Same class of bug via the JSON upload path: the address is echoed back +# with GNUNET_JSON_pack_object_incref(), which GNUNET_assert()s on a +# non-object, so a JSON body that is not an object used to abort the +# daemon. +for BODY in '[1,2]' '"a string"' 'null' +do + echo -n "Submitting a JSON address that is not an object (${BODY})..." + STATUS=$(curl "${BURL}/challenge/${NONCE}" \ + -X POST \ + -H "Accept: application/json" \ + -H "Content-Type: application/json" \ + -d "${BODY}" \ + -w "%{http_code}" -s -o $LAST_RESPONSE) \ + || exit_fail "challenger-httpd died on a non-object JSON address ${BODY}" + if [ "$STATUS" != "400" ] + then + exit_fail "Expected 400 Bad Request. Got: $STATUS" $(cat $LAST_RESPONSE) + fi + echo " OK" + + echo -n "Checking that challenger-httpd is still alive..." + wget --tries=1 --timeout=5 "${BURL}/config" -o /dev/null -O /dev/null \ + || exit_fail "challenger-httpd died on a non-object JSON address ${BODY}" + echo " OK" +done + # The rejected requests must not have consumed anything: a well-formed # address is still accepted afterwards. echo -n "Submitting a well-formed address..." diff --git a/src/challenger/test-challenger-exhaustion.sh b/src/challenger/test-challenger-exhaustion.sh @@ -310,4 +310,61 @@ then fi echo " OK" +# Now the other end of the scale: a validation with nothing left at all. +echo -n "Driving a validation to terminal exhaustion ..." +new_validation +for a in one two three +do + submit_address "${a}" + expect_transmitted "true" "the submission of address '${a}'" + burn_guesses +done +# Spend what remains of the PIN transmission budget. burn_guesses() cannot +# be reused for the last round: once addresses, transmissions and guesses +# are all spent, the response that consumes the final guess is already the +# terminal one, so it is a 429 rather than the 403 burn_guesses() expects. +while true +do + TRANSMISSIONS_LEFT=$(jq -r .pin_transmissions_left < "$LAST_RESPONSE") + if [ "${TRANSMISSIONS_LEFT}" -le 0 ] + then + break + fi + await_retransmission + submit_address "three" + expect_transmitted "true" "a retransmission while spending the budget" + for i in 1 2 3 + do + solve "$(( (10#${PIN} + i) % 100000000 ))" + done +done +echo " OK" + +# Since v8 this uses the same InvalidPinResponse shape as every other +# /solve failure, so that a client only ever parses one body. +echo -n "Terminal exhaustion uses the InvalidPinResponse shape ..." +solve "11111111" +CODE=$(jq -r .code < "$LAST_RESPONSE") +if [ "$STATUS" != "429" ] || [ "$CODE" != "9757" ] +then + exit_fail "/solve: expected 429 with code 9757 (TOO_MANY_ATTEMPTS) once nothing is left. Got: $STATUS / $CODE" $(cat $LAST_RESPONSE) +fi +if [ "$(jq -r .type < "$LAST_RESPONSE")" != "pending" ] +then + exit_fail "/solve: terminal 429 must use the InvalidPinResponse shape. Got: $(cat $LAST_RESPONSE)" +fi +for field in addresses_left pin_transmissions_left auth_attempts_left +do + if [ "$(jq -r ".${field}" < "$LAST_RESPONSE")" != "0" ] + then + exit_fail "/solve: expected ${field}=0 on the terminal 429. Got: $(cat $LAST_RESPONSE)" + fi +done +if [ "$(jq -r .exhausted < "$LAST_RESPONSE")" != "true" ] || \ + [ "$(jq -r .no_challenge < "$LAST_RESPONSE")" != "false" ] +then + exit_fail "/solve: expected exhausted=true and no_challenge=false. Got: $(cat $LAST_RESPONSE)" +fi +echo " OK" + exit 0