diff options
Diffstat (limited to 'src/backend/anastasis-httpd_truth-solve.c')
-rw-r--r-- | src/backend/anastasis-httpd_truth-solve.c | 1474 |
1 files changed, 1474 insertions, 0 deletions
diff --git a/src/backend/anastasis-httpd_truth-solve.c b/src/backend/anastasis-httpd_truth-solve.c new file mode 100644 index 0000000..eb09dc7 --- /dev/null +++ b/src/backend/anastasis-httpd_truth-solve.c @@ -0,0 +1,1474 @@ +/* + This file is part of Anastasis + Copyright (C) 2019-2022 Anastasis SARL + + Anastasis is free software; you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + Anastasis is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License along with + Anastasis; see the file COPYING. If not, see <http://www.gnu.org/licenses/> +*/ +/** + * @file anastasis-httpd_truth-solve.c + * @brief functions to handle incoming requests on /truth/$TID/solve + * @author Dennis Neufeld + * @author Dominik Meister + * @author Christian Grothoff + */ +#include "platform.h" +#include "anastasis-httpd.h" +#include "anastasis_service.h" +#include "anastasis-httpd_truth.h" +#include <gnunet/gnunet_util_lib.h> +#include <gnunet/gnunet_rest_lib.h> +#include "anastasis_authorization_lib.h" +#include <taler/taler_merchant_service.h> +#include <taler/taler_json_lib.h> +#include <taler/taler_mhd_lib.h> + +/** + * What is the maximum frequency at which we allow + * clients to attempt to answer security questions? + */ +#define MAX_QUESTION_FREQ GNUNET_TIME_relative_multiply ( \ + GNUNET_TIME_UNIT_SECONDS, 30) + +/** + * How long should the wallet check for auto-refunds before giving up? + */ +#define AUTO_REFUND_TIMEOUT GNUNET_TIME_relative_multiply ( \ + GNUNET_TIME_UNIT_MINUTES, 2) + + +/** + * How many retries do we allow per code? + */ +#define INITIAL_RETRY_COUNTER 3 + + +struct SolveContext +{ + + /** + * Payment Identifier + */ + struct ANASTASIS_PaymentSecretP payment_identifier; + + /** + * Public key of the challenge which is solved. + */ + struct ANASTASIS_CRYPTO_TruthUUIDP truth_uuid; + + /** + * Key to decrypt the truth. + */ + struct ANASTASIS_CRYPTO_TruthKeyP truth_key; + + /** + * Cost for paying the challenge. + */ + struct TALER_Amount challenge_cost; + + /** + * Our handler context. + */ + struct TM_HandlerContext *hc; + + /** + * Opaque parsing context. + */ + void *opaque_post_parsing_context; + + /** + * Uploaded JSON data, NULL if upload is not yet complete. + */ + json_t *root; + + /** + * Kept in DLL for shutdown handling while suspended. + */ + struct SolveContext *next; + + /** + * Kept in DLL for shutdown handling while suspended. + */ + struct SolveContext *prev; + + /** + * Connection handle for closing or resuming + */ + struct MHD_Connection *connection; + + /** + * Reference to the authorization plugin which was loaded + */ + struct ANASTASIS_AuthorizationPlugin *authorization; + + /** + * Status of the authorization + */ + struct ANASTASIS_AUTHORIZATION_State *as; + + /** + * Used while we are awaiting proposal creation. + */ + struct TALER_MERCHANT_PostOrdersHandle *po; + + /** + * Used while we are waiting payment. + */ + struct TALER_MERCHANT_OrderMerchantGetHandle *cpo; + + /** + * HTTP response code to use on resume, if non-NULL. + */ + struct MHD_Response *resp; + + /** + * Our entry in the #to_heap, or NULL. + */ + struct GNUNET_CONTAINER_HeapNode *hn; + + /** + * Challenge response we got from the request. + */ + struct GNUNET_HashCode challenge_response; + + /** + * How long do we wait at most for payment or + * authorization? + */ + struct GNUNET_TIME_Absolute timeout; + + /** + * Random authorization code we are using. + */ + uint64_t code; + + /** + * HTTP response code to use on resume, if resp is set. + */ + unsigned int response_code; + + /** + * true if client did not provide a payment secret / order ID. + */ + bool no_payment_identifier_provided; + + /** + * True if this entry is in the #gc_head DLL. + */ + bool in_list; + + /** + * True if this entry is currently suspended. + */ + bool suspended; + +}; + + +/** + * Head of linked list over all authorization processes + */ +static struct SolveContext *gc_head; + +/** + * Tail of linked list over all authorization processes + */ +static struct SolveContext *gc_tail; + +/** + * Task running #do_timeout(). + */ +static struct GNUNET_SCHEDULER_Task *to_task; + + +/** + * Generate a response telling the client that answering this + * challenge failed because the rate limit has been exceeded. + * + * @param gc request to answer for + * @return MHD status code + */ +static MHD_RESULT +reply_rate_limited (const struct SolveContext *gc) +{ + if (NULL != gc->authorization) + return TALER_MHD_REPLY_JSON_PACK ( + gc->connection, + MHD_HTTP_TOO_MANY_REQUESTS, + TALER_MHD_PACK_EC (TALER_EC_ANASTASIS_TRUTH_RATE_LIMITED), + GNUNET_JSON_pack_uint64 ("request_limit", + gc->authorization->retry_counter), + GNUNET_JSON_pack_time_rel ("request_frequency", + gc->authorization->code_rotation_period)); + /* must be security question */ + return TALER_MHD_REPLY_JSON_PACK ( + gc->connection, + MHD_HTTP_TOO_MANY_REQUESTS, + TALER_MHD_PACK_EC (TALER_EC_ANASTASIS_TRUTH_RATE_LIMITED), + GNUNET_JSON_pack_uint64 ("request_limit", + INITIAL_RETRY_COUNTER), + GNUNET_JSON_pack_time_rel ("request_frequency", + MAX_QUESTION_FREQ)); +} + + +/** + * Timeout requests that are past their due date. + * + * @param cls NULL + */ +static void +do_timeout (void *cls) +{ + struct SolveContext *gc; + + (void) cls; + to_task = NULL; + while (NULL != + (gc = GNUNET_CONTAINER_heap_peek (AH_to_heap))) + { + if (GNUNET_TIME_absolute_is_future (gc->timeout)) + break; + if (gc->suspended) + { + /* Test needed as we may have a "concurrent" + wakeup from another task that did not clear + this entry from the heap before the + response process concluded. */ + gc->suspended = false; + MHD_resume_connection (gc->connection); + } + GNUNET_assert (NULL != gc->hn); + gc->hn = NULL; + GNUNET_assert (gc == + GNUNET_CONTAINER_heap_remove_root (AH_to_heap)); + } + if (NULL == gc) + return; + to_task = GNUNET_SCHEDULER_add_at (gc->timeout, + &do_timeout, + NULL); +} + + +void +AH_truth_solve_shutdown (void) +{ + struct SolveContext *gc; + + while (NULL != (gc = gc_head)) + { + GNUNET_CONTAINER_DLL_remove (gc_head, + gc_tail, + gc); + gc->in_list = false; + if (NULL != gc->cpo) + { + TALER_MERCHANT_merchant_order_get_cancel (gc->cpo); + gc->cpo = NULL; + } + if (NULL != gc->po) + { + TALER_MERCHANT_orders_post_cancel (gc->po); + gc->po = NULL; + } + if (gc->suspended) + { + gc->suspended = false; + MHD_resume_connection (gc->connection); + } + if (NULL != gc->as) + { + gc->authorization->cleanup (gc->as); + gc->as = NULL; + gc->authorization = NULL; + } + } + ANASTASIS_authorization_plugin_shutdown (); + if (NULL != to_task) + { + GNUNET_SCHEDULER_cancel (to_task); + to_task = NULL; + } +} + + +/** + * Callback used to notify the application about completed requests. + * Cleans up the requests data structures. + * + * @param[in,out] hc + */ +static void +request_done (struct TM_HandlerContext *hc) +{ + struct SolveContext *gc = hc->ctx; + + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Request completed\n"); + if (NULL == gc) + return; + hc->cc = NULL; + GNUNET_assert (! gc->suspended); + if (gc->in_list) + { + GNUNET_CONTAINER_DLL_remove (gc_head, + gc_tail, + gc); + gc->in_list = false; + } + if (NULL != gc->hn) + { + GNUNET_assert (gc == + GNUNET_CONTAINER_heap_remove_node (gc->hn)); + gc->hn = NULL; + } + if (NULL != gc->as) + { + gc->authorization->cleanup (gc->as); + gc->authorization = NULL; + gc->as = NULL; + } + if (NULL != gc->cpo) + { + TALER_MERCHANT_merchant_order_get_cancel (gc->cpo); + gc->cpo = NULL; + } + if (NULL != gc->po) + { + TALER_MERCHANT_orders_post_cancel (gc->po); + gc->po = NULL; + } + if (NULL != gc->root) + { + json_decref (gc->root); + gc->root = NULL; + } + TALER_MHD_parse_post_cleanup_callback (gc->opaque_post_parsing_context); + GNUNET_free (gc); + hc->ctx = NULL; +} + + +/** + * Transmit a payment request for @a order_id on @a connection + * + * @param gc context to make payment request for + */ +static void +make_payment_request (struct SolveContext *gc) +{ + struct MHD_Response *resp; + + resp = MHD_create_response_from_buffer (0, + NULL, + MHD_RESPMEM_PERSISTENT); + GNUNET_assert (NULL != resp); + TALER_MHD_add_global_headers (resp); + { + char *hdr; + char *order_id; + const char *pfx; + const char *hn; + + if (0 == strncasecmp ("https://", + AH_backend_url, + strlen ("https://"))) + { + pfx = "taler://"; + hn = &AH_backend_url[strlen ("https://")]; + } + else if (0 == strncasecmp ("http://", + AH_backend_url, + strlen ("http://"))) + { + pfx = "taler+http://"; + hn = &AH_backend_url[strlen ("http://")]; + } + else + { + /* This invariant holds as per check in anastasis-httpd.c */ + GNUNET_assert (0); + } + /* This invariant holds as per check in anastasis-httpd.c */ + GNUNET_assert (0 != strlen (hn)); + + order_id = GNUNET_STRINGS_data_to_string_alloc ( + &gc->payment_identifier, + sizeof (gc->payment_identifier)); + GNUNET_asprintf (&hdr, + "%spay/%s%s/", + pfx, + hn, + order_id); + GNUNET_free (order_id); + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Sending payment request `%s'\n", + hdr); + GNUNET_break (MHD_YES == + MHD_add_response_header (resp, + ANASTASIS_HTTP_HEADER_TALER, + hdr)); + GNUNET_free (hdr); + } + gc->resp = resp; + gc->response_code = MHD_HTTP_PAYMENT_REQUIRED; +} + + +/** + * Callbacks of this type are used to serve the result of submitting a + * /contract request to a merchant. + * + * @param cls our `struct SolveContext` + * @param por response details + */ +static void +proposal_cb (void *cls, + const struct TALER_MERCHANT_PostOrdersReply *por) +{ + struct SolveContext *gc = cls; + enum GNUNET_DB_QueryStatus qs; + + gc->po = NULL; + GNUNET_assert (gc->in_list); + GNUNET_CONTAINER_DLL_remove (gc_head, + gc_tail, + gc); + gc->in_list = false; + GNUNET_assert (gc->suspended); + gc->suspended = false; + MHD_resume_connection (gc->connection); + AH_trigger_daemon (NULL); + if (MHD_HTTP_OK != por->hr.http_status) + { + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Backend returned status %u/%d\n", + por->hr.http_status, + (int) por->hr.ec); + GNUNET_break (0); + gc->resp = TALER_MHD_MAKE_JSON_PACK ( + GNUNET_JSON_pack_uint64 ("code", + TALER_EC_ANASTASIS_TRUTH_PAYMENT_CREATE_BACKEND_ERROR), + GNUNET_JSON_pack_string ("hint", + "Failed to setup order with merchant backend"), + GNUNET_JSON_pack_uint64 ("backend-ec", + por->hr.ec), + GNUNET_JSON_pack_uint64 ("backend-http-status", + por->hr.http_status), + GNUNET_JSON_pack_allow_null ( + GNUNET_JSON_pack_object_steal ("backend-reply", + (json_t *) por->hr.reply))); + gc->response_code = MHD_HTTP_BAD_GATEWAY; + return; + } + qs = db->record_challenge_payment (db->cls, + &gc->truth_uuid, + &gc->payment_identifier, + &gc->challenge_cost); + if (0 >= qs) + { + GNUNET_break (0); + gc->resp = TALER_MHD_make_error (TALER_EC_GENERIC_DB_STORE_FAILED, + "record challenge payment"); + gc->response_code = MHD_HTTP_INTERNAL_SERVER_ERROR; + return; + } + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Setup fresh order, creating payment request\n"); + make_payment_request (gc); +} + + +/** + * Callback to process a GET /check-payment request + * + * @param cls our `struct SolveContext` + * @param osr order status + */ +static void +check_payment_cb (void *cls, + const struct TALER_MERCHANT_OrderStatusResponse *osr) + +{ + struct SolveContext *gc = cls; + const struct TALER_MERCHANT_HttpResponse *hr = &osr->hr; + + gc->cpo = NULL; + GNUNET_assert (gc->in_list); + GNUNET_CONTAINER_DLL_remove (gc_head, + gc_tail, + gc); + gc->in_list = false; + GNUNET_assert (gc->suspended); + gc->suspended = false; + MHD_resume_connection (gc->connection); + AH_trigger_daemon (NULL); + + switch (hr->http_status) + { + case MHD_HTTP_OK: + GNUNET_assert (NULL != osr); + break; + case MHD_HTTP_NOT_FOUND: + /* We created this order before, how can it be not found now? */ + GNUNET_break (0); + gc->resp = TALER_MHD_make_error (TALER_EC_ANASTASIS_TRUTH_ORDER_DISAPPEARED, + NULL); + gc->response_code = MHD_HTTP_BAD_GATEWAY; + return; + case MHD_HTTP_BAD_GATEWAY: + gc->resp = TALER_MHD_make_error ( + TALER_EC_ANASTASIS_TRUTH_BACKEND_EXCHANGE_BAD, + NULL); + gc->response_code = MHD_HTTP_BAD_GATEWAY; + return; + case MHD_HTTP_GATEWAY_TIMEOUT: + gc->resp = TALER_MHD_make_error (TALER_EC_ANASTASIS_GENERIC_BACKEND_TIMEOUT, + "Timeout check payment status"); + GNUNET_assert (NULL != gc->resp); + gc->response_code = MHD_HTTP_GATEWAY_TIMEOUT; + return; + default: + { + char status[14]; + + GNUNET_snprintf (status, + sizeof (status), + "%u", + hr->http_status); + gc->resp = TALER_MHD_make_error ( + TALER_EC_ANASTASIS_TRUTH_UNEXPECTED_PAYMENT_STATUS, + status); + GNUNET_assert (NULL != gc->resp); + gc->response_code = MHD_HTTP_INTERNAL_SERVER_ERROR; + return; + } + } + + GNUNET_assert (MHD_HTTP_OK == hr->http_status); + switch (osr->details.ok.status) + { + case TALER_MERCHANT_OSC_PAID: + { + enum GNUNET_DB_QueryStatus qs; + + qs = db->update_challenge_payment (db->cls, + &gc->truth_uuid, + &gc->payment_identifier); + if (0 <= qs) + { + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Order has been paid, continuing with request processing\n"); + return; /* continue as planned */ + } + GNUNET_break (0); + gc->resp = TALER_MHD_make_error (TALER_EC_GENERIC_DB_STORE_FAILED, + "update challenge payment"); + gc->response_code = MHD_HTTP_INTERNAL_SERVER_ERROR; + return; /* continue as planned */ + } + case TALER_MERCHANT_OSC_CLAIMED: + case TALER_MERCHANT_OSC_UNPAID: + /* repeat payment request */ + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Order remains unpaid, sending payment request again\n"); + make_payment_request (gc); + return; + } + /* should never get here */ + GNUNET_break (0); +} + + +/** + * Helper function used to ask our backend to begin processing a + * payment for the user's account. May perform asynchronous + * operations by suspending the connection if required. + * + * @param gc context to begin payment for. + * @return MHD status code + */ +static MHD_RESULT +begin_payment (struct SolveContext *gc) +{ + enum GNUNET_DB_QueryStatus qs; + char *order_id; + + qs = db->lookup_challenge_payment (db->cls, + &gc->truth_uuid, + &gc->payment_identifier); + if (qs < 0) + { + GNUNET_break (0); + return TALER_MHD_reply_with_error (gc->connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + "lookup challenge payment"); + } + GNUNET_assert (! gc->in_list); + gc->in_list = true; + GNUNET_CONTAINER_DLL_insert (gc_tail, + gc_head, + gc); + GNUNET_assert (! gc->suspended); + gc->suspended = true; + MHD_suspend_connection (gc->connection); + if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT == qs) + { + /* We already created the order, check if it was paid */ + struct GNUNET_TIME_Relative timeout; + + order_id = GNUNET_STRINGS_data_to_string_alloc ( + &gc->payment_identifier, + sizeof (gc->payment_identifier)); + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Order exists, checking payment status for order `%s'\n", + order_id); + timeout = GNUNET_TIME_absolute_get_remaining (gc->timeout); + gc->cpo = TALER_MERCHANT_merchant_order_get (AH_ctx, + AH_backend_url, + order_id, + NULL /* NOT session-bound */, + timeout, + &check_payment_cb, + gc); + } + else + { + /* Create a fresh order */ + static const char *no_uuids[1] = { NULL }; + json_t *order; + struct GNUNET_TIME_Timestamp pay_deadline; + + GNUNET_CRYPTO_random_block (GNUNET_CRYPTO_QUALITY_WEAK, + &gc->payment_identifier, + sizeof (struct ANASTASIS_PaymentSecretP)); + order_id = GNUNET_STRINGS_data_to_string_alloc ( + &gc->payment_identifier, + sizeof (gc->payment_identifier)); + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Creating fresh order `%s'\n", + order_id); + pay_deadline = GNUNET_TIME_relative_to_timestamp ( + ANASTASIS_CHALLENGE_OFFER_LIFETIME); + order = GNUNET_JSON_PACK ( + TALER_JSON_pack_amount ("amount", + &gc->challenge_cost), + GNUNET_JSON_pack_string ("summary", + "challenge fee for anastasis service"), + GNUNET_JSON_pack_string ("order_id", + order_id), + GNUNET_JSON_pack_time_rel ("auto_refund", + AUTO_REFUND_TIMEOUT), + GNUNET_JSON_pack_timestamp ("pay_deadline", + pay_deadline)); + gc->po = TALER_MERCHANT_orders_post2 (AH_ctx, + AH_backend_url, + order, + AUTO_REFUND_TIMEOUT, + NULL, /* no payment target */ + 0, + NULL, /* no inventory products */ + 0, + no_uuids, /* no uuids */ + false, /* do NOT require claim token */ + &proposal_cb, + gc); + json_decref (order); + } + GNUNET_free (order_id); + AH_trigger_curl (); + return MHD_YES; +} + + +/** + * Load encrypted keyshare from db and return it to the client. + * + * @param truth_uuid UUID to the truth for the looup + * @param connection the connection to respond upon + * @return MHD status code + */ +static MHD_RESULT +return_key_share ( + const struct ANASTASIS_CRYPTO_TruthUUIDP *truth_uuid, + struct MHD_Connection *connection) +{ + struct ANASTASIS_CRYPTO_EncryptedKeyShareP encrypted_keyshare; + + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Returning key share of %s\n", + TALER_B2S (truth_uuid)); + { + enum GNUNET_DB_QueryStatus qs; + + qs = db->get_key_share (db->cls, + truth_uuid, + &encrypted_keyshare); + switch (qs) + { + case GNUNET_DB_STATUS_HARD_ERROR: + case GNUNET_DB_STATUS_SOFT_ERROR: + GNUNET_break (0); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + "get key share"); + case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: + /* this should be "impossible", after all the + client was able to solve the challenge! + (Exception: we deleted the truth via GC + just while the client was trying to recover. + Alas, highly unlikely...) */ + GNUNET_break (0); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_NOT_FOUND, + TALER_EC_ANASTASIS_TRUTH_KEY_SHARE_GONE, + NULL); + case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: + break; + } + } + + { + struct MHD_Response *resp; + MHD_RESULT ret; + + resp = MHD_create_response_from_buffer (sizeof (encrypted_keyshare), + &encrypted_keyshare, + MHD_RESPMEM_MUST_COPY); + TALER_MHD_add_global_headers (resp); + ret = MHD_queue_response (connection, + MHD_HTTP_OK, + resp); + MHD_destroy_response (resp); + return ret; + } +} + + +/** + * 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 SolveContext *gc) +{ + GNUNET_assert (NULL == gc->hn); + GNUNET_assert (! gc->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 SolveContext *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. + * + * @param connection the connection we are handling + * @param gc our overall handler context + */ +static MHD_RESULT +run_authorization_process (struct MHD_Connection *connection, + struct SolveContext *gc) +{ + enum ANASTASIS_AUTHORIZATION_SolveResult ret; + + GNUNET_assert (! gc->suspended); + if (NULL == gc->authorization->solve) + { + GNUNET_break (0); + return TALER_MHD_reply_with_error (gc->connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_ANASTASIS_TRUTH_AUTHORIZATION_START_FAILED, + "solve method not implemented for authorization method"); + } + ret = gc->authorization->solve (gc->as, + gc->timeout, + &gc->challenge_response, + connection); + switch (ret) + { + case ANASTASIS_AUTHORIZATION_SRES_SUSPENDED: + /* connection was suspended */ + gc_suspended (gc); + return MHD_YES; + case ANASTASIS_AUTHORIZATION_SRES_FAILED: + gc->authorization->cleanup (gc->as); + gc->as = NULL; + return MHD_YES; + case ANASTASIS_AUTHORIZATION_SRES_FAILED_REPLY_FAILED: + gc->authorization->cleanup (gc->as); + gc->as = NULL; + return MHD_NO; + case ANASTASIS_AUTHORIZATION_SRES_FINISHED: + GNUNET_assert (! gc->suspended); + gc->authorization->cleanup (gc->as); + gc->as = NULL; + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Resuming with authorization successful!\n"); + if (gc->in_list) + { + GNUNET_CONTAINER_DLL_remove (gc_head, + gc_tail, + gc); + gc->in_list = false; + } + return MHD_YES; + } + GNUNET_break (0); + return MHD_NO; +} + + +/** + * 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 SolveContext *gc) +{ + enum GNUNET_DB_QueryStatus qs; + struct GNUNET_TIME_Timestamp rt; + uint64_t code; + enum ANASTASIS_DB_CodeStatus cs; + struct GNUNET_HashCode hc; + bool satisfied; + uint64_t dummy; + + rt = GNUNET_TIME_UNIT_FOREVER_TS; + 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 == + reply_rate_limited (gc)) + ? GNUNET_NO + : GNUNET_SYSERR; + } + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Using intentionally wrong answer to produce rate-limiting\n"); + /* 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 == + reply_rate_limited (gc)) + ? 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 SolveContext *gc, + const void *decrypted_truth, + size_t decrypted_truth_size) +{ + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Handling security question challenge\n"); + /* 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. Also ensures that the + * request is rate-limited. + * + * @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 SolveContext *gc, + const void *decrypted_truth, + size_t decrypted_truth_size) +{ + /* Non-random code, call plugin directly! */ + enum ANASTASIS_AUTHORIZATION_SolveResult aar; + enum GNUNET_GenericReturnValue ret; + + ret = rate_limit (gc); + if (GNUNET_OK != ret) + return (GNUNET_NO == ret) ? 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); + } + if (NULL == gc->authorization->solve) + { + GNUNET_break (0); + return TALER_MHD_reply_with_error (gc->connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_ANASTASIS_TRUTH_AUTHORIZATION_START_FAILED, + "solve method not implemented for authorization method"); + } + aar = gc->authorization->solve (gc->as, + gc->timeout, + &gc->challenge_response, + gc->connection); + switch (aar) + { + case ANASTASIS_AUTHORIZATION_SRES_FAILED: + return MHD_YES; + case ANASTASIS_AUTHORIZATION_SRES_SUSPENDED: + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Suspending request handling\n"); + gc_suspended (gc); + return MHD_YES; + case ANASTASIS_AUTHORIZATION_SRES_FAILED_REPLY_FAILED: + return MHD_NO; + case ANASTASIS_AUTHORIZATION_SRES_FINISHED: + return return_key_share (&gc->truth_uuid, + gc->connection); + } + GNUNET_break (0); + return MHD_NO; +} + + +/** + * Handle special case of an answer being checked + * by the plugin asynchronously (IBAN) after we inverted + * the hash using the database. + * + * @param[in,out] gc request to handle + * @param code validation code provided by the client + * @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 +iban_validation (struct SolveContext *gc, + uint64_t code, + const void *decrypted_truth, + size_t decrypted_truth_size) +{ + enum ANASTASIS_AUTHORIZATION_SolveResult aar; + + gc->as = gc->authorization->start (gc->authorization->cls, + &AH_trigger_daemon, + NULL, + &gc->truth_uuid, + code, + 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); + } + if (NULL == gc->authorization->solve) + { + GNUNET_break (0); + return TALER_MHD_reply_with_error (gc->connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_ANASTASIS_TRUTH_AUTHORIZATION_START_FAILED, + "solve method not implemented for authorization method"); + } + aar = gc->authorization->solve (gc->as, + gc->timeout, + &gc->challenge_response, + gc->connection); + switch (aar) + { + case ANASTASIS_AUTHORIZATION_SRES_FAILED: + return MHD_YES; + case ANASTASIS_AUTHORIZATION_SRES_SUSPENDED: + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Suspending request handling\n"); + gc_suspended (gc); + return MHD_YES; + case ANASTASIS_AUTHORIZATION_SRES_FAILED_REPLY_FAILED: + return MHD_NO; + case ANASTASIS_AUTHORIZATION_SRES_FINISHED: + return return_key_share (&gc->truth_uuid, + gc->connection); + } + GNUNET_break (0); + return MHD_NO; +} + + +MHD_RESULT +AH_handler_truth_solve ( + struct MHD_Connection *connection, + struct TM_HandlerContext *hc, + const struct ANASTASIS_CRYPTO_TruthUUIDP *truth_uuid, + const char *upload_data, + size_t *upload_data_size) +{ + struct SolveContext *gc = hc->ctx; + void *encrypted_truth; + size_t encrypted_truth_size; + void *decrypted_truth; + size_t decrypted_truth_size; + char *truth_mime = NULL; + bool is_question; + + if (NULL == gc) + { + /* Fresh request, do initial setup */ + gc = GNUNET_new (struct SolveContext); + gc->hc = hc; + hc->ctx = gc; + gc->connection = connection; + gc->truth_uuid = *truth_uuid; + gc->hc->cc = &request_done; + gc->timeout = GNUNET_TIME_relative_to_absolute ( + GNUNET_TIME_UNIT_SECONDS); + TALER_MHD_parse_request_timeout (connection, + &gc->timeout); + } /* end of first-time initialization (if NULL == gc) */ + else + { + /* might have been woken up by authorization plugin, + so clear the flag. MDH called us, so we are + clearly no longer suspended */ + gc->suspended = false; + if (NULL != gc->resp) + { + MHD_RESULT ret; + + /* We generated a response asynchronously, queue that */ + ret = MHD_queue_response (connection, + gc->response_code, + gc->resp); + GNUNET_break (MHD_YES == ret); + MHD_destroy_response (gc->resp); + gc->resp = NULL; + return ret; + } + if (NULL != gc->as) + { + /* Authorization process is "running", check what is going on */ + GNUNET_assert (NULL != gc->authorization); + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Continuing with running the authorization process\n"); + GNUNET_assert (! gc->suspended); + return run_authorization_process (connection, + gc); + } + /* We get here if the async check for payment said this request + was indeed paid! */ + } + + if (NULL == gc->root) + { + /* parse byte stream upload into JSON */ + enum GNUNET_GenericReturnValue res; + + res = TALER_MHD_parse_post_json (connection, + &gc->opaque_post_parsing_context, + upload_data, + upload_data_size, + &gc->root); + if (GNUNET_SYSERR == res) + { + GNUNET_assert (NULL == gc->root); + return MHD_NO; /* bad upload, could not even generate error */ + } + if ( (GNUNET_NO == res) || + (NULL == gc->root) ) + { + GNUNET_assert (NULL == gc->root); + return MHD_YES; /* so far incomplete upload or parser error */ + } + + /* 'root' is now initialized, parse JSON body */ + { + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_fixed_auto ("truth_decryption_key", + &gc->truth_key), + GNUNET_JSON_spec_fixed_auto ("h_response", + &gc->challenge_response), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_fixed_auto ("payment_secret", + &gc->payment_identifier), + &gc->no_payment_identifier_provided), + GNUNET_JSON_spec_end () + }; + enum GNUNET_GenericReturnValue res; + + res = TALER_MHD_parse_json_data (connection, + gc->root, + spec); + if (GNUNET_SYSERR == res) + { + GNUNET_break (0); + return MHD_NO; /* hard failure */ + } + if (GNUNET_NO == res) + { + GNUNET_break_op (0); + return MHD_YES; /* failure */ + } + if (! gc->no_payment_identifier_provided) + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Client provided payment identifier `%s'\n", + TALER_B2S (&gc->payment_identifier)); + } + } + + { + /* load encrypted truth from DB; we may do this repeatedly + while handling the same request, if payment was checked + asynchronously! */ + enum GNUNET_DB_QueryStatus qs; + char *method; + + qs = db->get_escrow_challenge (db->cls, + &gc->truth_uuid, + &encrypted_truth, + &encrypted_truth_size, + &truth_mime, + &method); + switch (qs) + { + case GNUNET_DB_STATUS_HARD_ERROR: + case GNUNET_DB_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, + "get escrow challenge"); + case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_NOT_FOUND, + TALER_EC_ANASTASIS_TRUTH_UNKNOWN, + NULL); + case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: + break; + } + is_question = (0 == strcmp ("question", + method)); + if (! is_question) + { + gc->authorization + = ANASTASIS_authorization_plugin_load (method, + db, + AH_cfg); + if (NULL == gc->authorization) + { + MHD_RESULT ret; + + ret = TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_ANASTASIS_TRUTH_AUTHORIZATION_METHOD_NO_LONGER_SUPPORTED, + method); + GNUNET_free (encrypted_truth); + GNUNET_free (truth_mime); + GNUNET_free (method); + return ret; + } + gc->challenge_cost = gc->authorization->cost; + } + else + { + gc->challenge_cost = AH_question_cost; + } + GNUNET_free (method); + } + + /* check for payment */ + if ( (is_question) || + (! gc->authorization->payment_plugin_managed) ) + { + if (! TALER_amount_is_zero (&gc->challenge_cost)) + { + /* Check database to see if the transaction is paid for */ + enum GNUNET_DB_QueryStatus qs; + bool paid; + + if (gc->no_payment_identifier_provided) + { + GNUNET_free (truth_mime); + GNUNET_free (encrypted_truth); + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Beginning payment, client did not provide payment identifier\n"); + return begin_payment (gc); + } + qs = db->check_challenge_payment (db->cls, + &gc->payment_identifier, + &gc->truth_uuid, + &paid); + switch (qs) + { + case GNUNET_DB_STATUS_HARD_ERROR: + case GNUNET_DB_STATUS_SOFT_ERROR: + GNUNET_break (0); + GNUNET_free (truth_mime); + GNUNET_free (encrypted_truth); + return TALER_MHD_reply_with_error (gc->connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + "check challenge payment"); + case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: + /* Create fresh payment identifier (cannot trust client) */ + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Client-provided payment identifier is unknown.\n"); + GNUNET_free (truth_mime); + GNUNET_free (encrypted_truth); + return begin_payment (gc); + case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: + if (! paid) + { + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Payment identifier known. Checking payment with client's payment identifier\n"); + GNUNET_free (truth_mime); + GNUNET_free (encrypted_truth); + return begin_payment (gc); + } + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Payment confirmed\n"); + break; + } + } + else + { + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Request is free of charge\n"); + } + } + + /* We've been paid, now validate the response */ + /* decrypt encrypted_truth */ + ANASTASIS_CRYPTO_truth_decrypt (&gc->truth_key, + encrypted_truth, + encrypted_truth_size, + &decrypted_truth, + &decrypted_truth_size); + GNUNET_free (encrypted_truth); + if (NULL == decrypted_truth) + { + /* most likely, the decryption key is simply wrong */ + GNUNET_break_op (0); + GNUNET_free (truth_mime); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_ANASTASIS_TRUTH_DECRYPTION_FAILED, + NULL); + } + + /* Special case for secure question: we do not generate a numeric challenge, + but check that the hash matches */ + if (is_question) + { + MHD_RESULT ret; + + ret = handle_security_question (gc, + decrypted_truth, + decrypted_truth_size); + GNUNET_free (truth_mime); + GNUNET_free (decrypted_truth); + return ret; + } + + /* Not security question, check for answer in DB */ + { + enum ANASTASIS_DB_CodeStatus cs; + bool satisfied = false; + uint64_t code; + + GNUNET_free (truth_mime); + if (gc->authorization->user_provided_code) + { + MHD_RESULT res; + + if (GNUNET_TIME_absolute_is_past (gc->timeout)) + { + GNUNET_free (decrypted_truth); + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Timeout with user provided code\n"); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_FORBIDDEN, + TALER_EC_ANASTASIS_IBAN_MISSING_TRANSFER, + "timeout awaiting validation"); + } + 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, + &code, + &satisfied); + switch (cs) + { + case ANASTASIS_DB_CODE_STATUS_CHALLENGE_CODE_MISMATCH: + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Provided response does not match our stored challenge\n"); + GNUNET_free (decrypted_truth); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_FORBIDDEN, + TALER_EC_ANASTASIS_TRUTH_CHALLENGE_FAILED, + NULL); + case ANASTASIS_DB_CODE_STATUS_HARD_ERROR: + case ANASTASIS_DB_CODE_STATUS_SOFT_ERROR: + GNUNET_break (0); + GNUNET_free (decrypted_truth); + 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: + GNUNET_free (decrypted_truth); + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Specified challenge code %s was not issued\n", + GNUNET_h2s (&gc->challenge_response)); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_FORBIDDEN, + TALER_EC_ANASTASIS_TRUTH_CHALLENGE_UNKNOWN, + "specific challenge code was not issued"); + case ANASTASIS_DB_CODE_STATUS_VALID_CODE_STORED: + if (! satisfied) + { + MHD_RESULT res; + + res = iban_validation (gc, + code, + decrypted_truth, + decrypted_truth_size); + GNUNET_free (decrypted_truth); + return res; + } + GNUNET_free (decrypted_truth); + return return_key_share (&gc->truth_uuid, + connection); + default: + GNUNET_break (0); + return MHD_NO; + } + } +} |