/* 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 */ /** * @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 #include #include "anastasis_authorization_lib.h" #include #include #include /** * 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 */, false, timeout, &check_payment_cb, gc); } else { /* Create a fresh order */ 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, NULL, /* 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; } } }