From 435950ee10fc3d58f7ff992a2c2a2a3f73efa806 Mon Sep 17 00:00:00 2001 From: Christian Grothoff Date: Tue, 28 Sep 2021 15:50:28 +0200 Subject: theoretically, this completes the TOTP implementation, alas completely untested --- .../anastasis_authorization_plugin_totp.c | 47 ++- src/backend/anastasis-httpd_truth.c | 374 ++++++++++++++------- src/include/anastasis_database_plugin.h | 2 +- src/reducer/anastasis_api_recovery_redux.c | 9 +- 4 files changed, 300 insertions(+), 132 deletions(-) diff --git a/src/authorization/anastasis_authorization_plugin_totp.c b/src/authorization/anastasis_authorization_plugin_totp.c index 6fcdd39..ee1ab3f 100644 --- a/src/authorization/anastasis_authorization_plugin_totp.c +++ b/src/authorization/anastasis_authorization_plugin_totp.c @@ -59,14 +59,14 @@ struct ANASTASIS_AUTHORIZATION_State struct ANASTASIS_CRYPTO_TruthUUIDP truth_uuid; /** - * Our context. + * Was the challenge satisfied? */ - const struct ANASTASIS_AuthorizationContext *ac; + struct GNUNET_HashCode valid_replies[TIME_INTERVAL_RANGE * 2 + 1]; /** - * Was the challenge satisfied? + * Our context. */ - bool ok; + const struct ANASTASIS_AuthorizationContext *ac; }; @@ -212,9 +212,9 @@ compute_totp (int time_off, * @param trigger_cls closure for @a trigger * @param truth_uuid Identifier of the challenge, to be (if possible) included in the * interaction with the user - * @param code set to secret code that the user provided to satisfy the challenge in - * the main anastasis protocol - * @param data input to validate (i.e. the shared secret) + * @param code always 0 (direct validation, backend does + * not generate a code in this mode) + * @param data truth for input to validate (i.e. the shared secret) * @param data_length number of bytes in @a data * @return state to track progress on the authorization operation, NULL on failure */ @@ -230,7 +230,9 @@ totp_start (void *cls, const struct ANASTASIS_AuthorizationContext *ac = cls; struct ANASTASIS_AUTHORIZATION_State *as; uint64_t want; + unsigned int off = 0; + GNUNET_assert (0 == code); as = GNUNET_new (struct ANASTASIS_AUTHORIZATION_State); as->ac = ac; as->truth_uuid = *truth_uuid; @@ -241,8 +243,8 @@ totp_start (void *cls, want = compute_totp (i, data, data_length); - if (code == want) - as->ok = true; + ANASTASIS_hash_answer (want, + &as->valid_replies[off++]); } return as; } @@ -264,9 +266,32 @@ totp_process (struct ANASTASIS_AUTHORIZATION_State *as, MHD_RESULT mres; const char *mime; const char *lang; + const char *challenge_response_s; + struct GNUNET_HashCode challenge_response; + + challenge_response_s = MHD_lookup_connection_value (connection, + MHD_GET_ARGUMENT_KIND, + "response"); + if ( (NULL == challenge_response_s) || + (GNUNET_OK != + GNUNET_CRYPTO_hash_from_string (challenge_response_s, + &challenge_response)) ) + { + GNUNET_break_op (0); + mres = TALER_MHD_reply_with_error (connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "response"); + if (MHD_YES != mres) + return ANASTASIS_AUTHORIZATION_RES_FAILED_REPLY_FAILED; + return ANASTASIS_AUTHORIZATION_RES_FAILED; - if (as->ok) - return ANASTASIS_AUTHORIZATION_RES_FINISHED; + } + for (unsigned int i = 0; i<=TIME_INTERVAL_RANGE * 2; i++) + if (0 == + GNUNET_memcmp (&challenge_response, + &as->valid_replies[i])) + return ANASTASIS_AUTHORIZATION_RES_FINISHED; mime = MHD_lookup_connection_value (connection, MHD_HEADER_KIND, MHD_HTTP_HEADER_ACCEPT); diff --git a/src/backend/anastasis-httpd_truth.c b/src/backend/anastasis-httpd_truth.c index 4dd3ddc..aedd0a2 100644 --- a/src/backend/anastasis-httpd_truth.c +++ b/src/backend/anastasis-httpd_truth.c @@ -881,6 +881,38 @@ return_key_share ( } +/** + * Mark @a gc as suspended and update the respective + * data structures and jobs. + * + * @param[in,out] gc context of the suspended operation + */ +static void +gc_suspended (struct GetContext *gc) +{ + gc->suspended = true; + if (NULL == AH_to_heap) + AH_to_heap = GNUNET_CONTAINER_heap_create ( + GNUNET_CONTAINER_HEAP_ORDER_MIN); + gc->hn = GNUNET_CONTAINER_heap_insert (AH_to_heap, + gc, + gc->timeout.abs_value_us); + if (NULL != to_task) + { + GNUNET_SCHEDULER_cancel (to_task); + to_task = NULL; + } + { + struct GetContext *rn; + + rn = GNUNET_CONTAINER_heap_peek (AH_to_heap); + to_task = GNUNET_SCHEDULER_add_at (rn->timeout, + &do_timeout, + NULL); + } +} + + /** * Run the authorization method-specific 'process' function and continue * based on its result with generating an HTTP response. @@ -923,26 +955,7 @@ run_authorization_process (struct MHD_Connection *connection, return MHD_YES; case ANASTASIS_AUTHORIZATION_RES_SUSPENDED: /* connection was suspended */ - gc->suspended = true; - if (NULL == AH_to_heap) - AH_to_heap = GNUNET_CONTAINER_heap_create ( - GNUNET_CONTAINER_HEAP_ORDER_MIN); - gc->hn = GNUNET_CONTAINER_heap_insert (AH_to_heap, - gc, - gc->timeout.abs_value_us); - if (NULL != to_task) - { - GNUNET_SCHEDULER_cancel (to_task); - to_task = NULL; - } - { - struct GetContext *rn; - - rn = GNUNET_CONTAINER_heap_peek (AH_to_heap); - to_task = GNUNET_SCHEDULER_add_at (rn->timeout, - &do_timeout, - NULL); - } + gc_suspended (gc); return MHD_YES; case ANASTASIS_AUTHORIZATION_RES_SUCCESS_REPLY_FAILED: /* Challenge sent successfully */ @@ -978,6 +991,212 @@ run_authorization_process (struct MHD_Connection *connection, } +/** + * Use the database to rate-limit queries to the + * authentication procedure, but without actually + * storing 'real' challenge codes. + * + * @param[in,out] gc context to rate limit requests for + * @return #GNUNET_OK if rate-limiting passes, + * #GNUNET_NO if a reply was sent (rate limited) + * #GNUNET_SYSERR if we failed and no reply + * was queued + */ +static enum GNUNET_GenericReturnValue +rate_limit (struct GetContext *gc) +{ + enum GNUNET_DB_QueryStatus qs; + struct GNUNET_TIME_Absolute rt; + uint64_t code; + enum ANASTASIS_DB_CodeStatus cs; + struct GNUNET_HashCode hc; + bool satisfied; + uint64_t dummy; + + rt = GNUNET_TIME_UNIT_FOREVER_ABS; + qs = db->create_challenge_code (db->cls, + &gc->truth_uuid, + MAX_QUESTION_FREQ, + GNUNET_TIME_UNIT_HOURS, + INITIAL_RETRY_COUNTER, + &rt, + &code); + if (0 > qs) + { + GNUNET_break (0 < qs); + return (MHD_YES == + TALER_MHD_reply_with_error (gc->connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + "create_challenge_code (for rate limiting)")) + ? GNUNET_NO + : GNUNET_SYSERR; + } + if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) + { + return (MHD_YES == + TALER_MHD_reply_with_error (gc->connection, + MHD_HTTP_TOO_MANY_REQUESTS, + TALER_EC_ANASTASIS_TRUTH_RATE_LIMITED, + NULL)) + ? GNUNET_NO + : GNUNET_SYSERR; + } + /* decrement trial counter */ + ANASTASIS_hash_answer (code + 1, /* always use wrong answer */ + &hc); + cs = db->verify_challenge_code (db->cls, + &gc->truth_uuid, + &hc, + &dummy, + &satisfied); + switch (cs) + { + case ANASTASIS_DB_CODE_STATUS_CHALLENGE_CODE_MISMATCH: + /* good, what we wanted */ + return GNUNET_OK; + case ANASTASIS_DB_CODE_STATUS_HARD_ERROR: + case ANASTASIS_DB_CODE_STATUS_SOFT_ERROR: + GNUNET_break (0); + return (MHD_YES == + TALER_MHD_reply_with_error (gc->connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + "verify_challenge_code")) + ? GNUNET_NO + : GNUNET_SYSERR; + case ANASTASIS_DB_CODE_STATUS_NO_RESULTS: + return (MHD_YES == + TALER_MHD_reply_with_error (gc->connection, + MHD_HTTP_TOO_MANY_REQUESTS, + TALER_EC_ANASTASIS_TRUTH_RATE_LIMITED, + NULL)) + ? GNUNET_NO + : GNUNET_SYSERR; + case ANASTASIS_DB_CODE_STATUS_VALID_CODE_STORED: + /* this should be impossible, we used code+1 */ + GNUNET_assert (0); + } + return GNUNET_SYSERR; +} + + +/** + * Handle special case of a security question where we do not + * generate a code. Rate limits answers against brute forcing. + * + * @param[in,out] gc request to handle + * @param decrypted_truth hash to check against + * @param decrypted_truth_size number of bytes in @a decrypted_truth + * @return MHD status code + */ +static MHD_RESULT +handle_security_question (struct GetContext *gc, + const void *decrypted_truth, + size_t decrypted_truth_size) +{ + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Handling security question challenge\n"); + if (! gc->have_response) + { + return TALER_MHD_reply_with_error (gc->connection, + MHD_HTTP_FORBIDDEN, + TALER_EC_ANASTASIS_TRUTH_CHALLENGE_RESPONSE_REQUIRED, + NULL); + } + /* rate limit */ + { + enum GNUNET_GenericReturnValue ret; + + ret = rate_limit (gc); + if (GNUNET_OK != ret) + return (GNUNET_NO == ret) ? MHD_YES : MHD_NO; + } + /* check reply matches truth */ + if ( (decrypted_truth_size != sizeof (struct GNUNET_HashCode)) || + (0 != memcmp (&gc->challenge_response, + decrypted_truth, + decrypted_truth_size)) ) + { + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Wrong answer provided to secure question had %u bytes, wanted %u\n", + (unsigned int) decrypted_truth_size, + (unsigned int) sizeof (struct GNUNET_HashCode)); + return TALER_MHD_reply_with_error (gc->connection, + MHD_HTTP_FORBIDDEN, + TALER_EC_ANASTASIS_TRUTH_CHALLENGE_FAILED, + NULL); + } + /* good, return the key share */ + return return_key_share (&gc->truth_uuid, + gc->connection); +} + + +/** + * Handle special case of an answer being directly checked by the + * plugin and not by our database. Rate limits answers against brute + * forcing. + * + * @param[in,out] gc request to handle + * @param decrypted_truth hash to check against + * @param decrypted_truth_size number of bytes in @a decrypted_truth + * @return MHD status code + */ +static MHD_RESULT +direct_validation (struct GetContext *gc, + const void *decrypted_truth, + size_t decrypted_truth_size) +{ + /* Non-random code, call plugin directly! */ + enum ANASTASIS_AUTHORIZATION_Result aar; + enum GNUNET_GenericReturnValue res; + + res = rate_limit (gc); + if (GNUNET_OK != res) + return (GNUNET_NO == res) ? MHD_YES : MHD_NO; + gc->as = gc->authorization->start (gc->authorization->cls, + &AH_trigger_daemon, + NULL, + &gc->truth_uuid, + 0LLU, + decrypted_truth, + decrypted_truth_size); + if (NULL == gc->as) + { + GNUNET_break (0); + return TALER_MHD_reply_with_error (gc->connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_ANASTASIS_TRUTH_AUTHORIZATION_START_FAILED, + NULL); + } + aar = gc->authorization->process (gc->as, + GNUNET_TIME_UNIT_ZERO_ABS, + gc->connection); + switch (aar) + { + case ANASTASIS_AUTHORIZATION_RES_SUCCESS: + GNUNET_break (0); + return MHD_YES; + case ANASTASIS_AUTHORIZATION_RES_FAILED: + return MHD_YES; + case ANASTASIS_AUTHORIZATION_RES_SUSPENDED: + gc_suspended (gc); + return MHD_YES; + case ANASTASIS_AUTHORIZATION_RES_SUCCESS_REPLY_FAILED: + GNUNET_break (0); + return MHD_NO; + case ANASTASIS_AUTHORIZATION_RES_FAILED_REPLY_FAILED: + return MHD_NO; + case ANASTASIS_AUTHORIZATION_RES_FINISHED: + return return_key_share (&gc->truth_uuid, + gc->connection); + } + GNUNET_break (0); + return MHD_NO; +} + + MHD_RESULT AH_handler_truth_get ( struct MHD_Connection *connection, @@ -1113,7 +1332,6 @@ AH_handler_truth_get ( GNUNET_TIME_UNIT_SECONDS); } } - } /* end of first-time initialization (if NULL == gc) */ else { @@ -1291,104 +1509,14 @@ AH_handler_truth_get ( but check that the hash matches */ if (is_question) { - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Handling security question challenge\n"); - if (! gc->have_response) - { - GNUNET_free (decrypted_truth); - GNUNET_free (truth_mime); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_FORBIDDEN, - TALER_EC_ANASTASIS_TRUTH_CHALLENGE_RESPONSE_REQUIRED, - NULL); - } + MHD_RESULT ret; - { - enum GNUNET_DB_QueryStatus qs; - struct GNUNET_TIME_Absolute rt; - uint64_t code; - enum ANASTASIS_DB_CodeStatus cs; - struct GNUNET_HashCode hc; - bool satisfied; - uint64_t dummy; - - rt = GNUNET_TIME_UNIT_FOREVER_ABS; - qs = db->create_challenge_code (db->cls, - &gc->truth_uuid, - MAX_QUESTION_FREQ, - GNUNET_TIME_UNIT_HOURS, - INITIAL_RETRY_COUNTER, - &rt, - &code); - if (0 > qs) - { - GNUNET_break (0 < qs); - GNUNET_free (decrypted_truth); - GNUNET_free (truth_mime); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_FETCH_FAILED, - "create_challenge_code (for rate limiting)"); - } - if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) - { - GNUNET_free (decrypted_truth); - GNUNET_free (truth_mime); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_TOO_MANY_REQUESTS, - TALER_EC_ANASTASIS_TRUTH_RATE_LIMITED, - NULL); - } - /* decrement trial counter */ - ANASTASIS_hash_answer (code + 1, /* always use wrong answer */ - &hc); - cs = db->verify_challenge_code (db->cls, - &gc->truth_uuid, - &hc, - &dummy, - &satisfied); - switch (cs) - { - case ANASTASIS_DB_CODE_STATUS_CHALLENGE_CODE_MISMATCH: - /* good, what we wanted */ - break; - case ANASTASIS_DB_CODE_STATUS_HARD_ERROR: - case ANASTASIS_DB_CODE_STATUS_SOFT_ERROR: - GNUNET_break (0); - return TALER_MHD_reply_with_error (gc->connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_FETCH_FAILED, - "verify_challenge_code"); - case ANASTASIS_DB_CODE_STATUS_NO_RESULTS: - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_TOO_MANY_REQUESTS, - TALER_EC_ANASTASIS_TRUTH_RATE_LIMITED, - NULL); - case ANASTASIS_DB_CODE_STATUS_VALID_CODE_STORED: - /* this should be impossible, we used code+1 */ - GNUNET_assert (0); - } - } - if ( (decrypted_truth_size != sizeof (struct GNUNET_HashCode)) || - (0 != memcmp (&gc->challenge_response, - decrypted_truth, - decrypted_truth_size)) ) - { - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Wrong answer provided to secure question had %u bytes, wanted %u\n", - (unsigned int) decrypted_truth_size, - (unsigned int) sizeof (struct GNUNET_HashCode)); - GNUNET_free (decrypted_truth); - GNUNET_free (truth_mime); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_FORBIDDEN, - TALER_EC_ANASTASIS_TRUTH_CHALLENGE_FAILED, - NULL); - } - GNUNET_free (decrypted_truth); + ret = handle_security_question (gc, + decrypted_truth, + decrypted_truth_size); GNUNET_free (truth_mime); - return return_key_share (&gc->truth_uuid, - connection); + GNUNET_free (decrypted_truth); + return ret; } /* Not security question, check for answer in DB */ @@ -1399,6 +1527,18 @@ AH_handler_truth_get ( uint64_t code; GNUNET_free (truth_mime); + if (gc->authorization->user_provided_code) + { + MHD_RESULT res; + + res = direct_validation (gc, + decrypted_truth, + decrypted_truth_size); + GNUNET_free (decrypted_truth); + return res; + } + + /* random code, check against database */ cs = db->verify_challenge_code (db->cls, &gc->truth_uuid, &gc->challenge_response, diff --git a/src/include/anastasis_database_plugin.h b/src/include/anastasis_database_plugin.h index 7bf91a2..cf5a69a 100644 --- a/src/include/anastasis_database_plugin.h +++ b/src/include/anastasis_database_plugin.h @@ -633,7 +633,7 @@ struct ANASTASIS_DatabasePlugin (*mark_challenge_code_satisfied)( void *cls, const struct ANASTASIS_CRYPTO_TruthUUIDP *truth_uuid, - const uint64_t code); + uint64_t code); /** diff --git a/src/reducer/anastasis_api_recovery_redux.c b/src/reducer/anastasis_api_recovery_redux.c index 897a6dd..8709cf9 100644 --- a/src/reducer/anastasis_api_recovery_redux.c +++ b/src/reducer/anastasis_api_recovery_redux.c @@ -1684,10 +1684,13 @@ select_challenge_cb (void *cls, json_object_set_new (sctx->state, "selected_challenge_uuid", GNUNET_JSON_from_data_auto (&cd->uuid))); - if (0 == strcmp ("question", - cd->type)) + if ( (0 == strcmp ("question", + cd->type)) || + (0 == strcmp ("totp", + cd->type)) ) { - /* security question, immediately request user to answer it */ + /* security question or TOTP: + immediately request user to answer it */ set_state (sctx->state, ANASTASIS_RECOVERY_STATE_CHALLENGE_SOLVING); sctx->cb (sctx->cb_cls, -- cgit v1.2.3