/*
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.success.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;
}
}
}