merchant

Merchant backend to process payments, run by merchants
Log | Files | Refs | Submodules | README | LICENSE

commit bfa7d7574be58fa401dc8a7e2602c8237a2299fc
parent 1c71613d16918ab9d97e75f336a393d79892485e
Author: Christian Grothoff <christian@grothoff.org>
Date:   Tue,  2 Sep 2025 23:00:14 +0200

finish (untested) background work for MFA logic (#9816/#9817)

Diffstat:
Msrc/backend/Makefile.am | 4++++
Msrc/backend/taler-merchant-httpd.c | 54++++++++++++++++++++++++++++++++++++++++++++++++++++--
Msrc/backend/taler-merchant-httpd.h | 15+++++++++++++++
Msrc/backend/taler-merchant-httpd_mfa.c | 985+++++++++++++++++++++++++++++++++++++------------------------------------------
Msrc/backend/taler-merchant-httpd_mfa.h | 72++++++++++++++++++++++++++++--------------------------------------------
Msrc/backenddb/pg_create_mfa_challenge.c | 8+++-----
Msrc/backenddb/pg_create_mfa_challenge.h | 6+++---
Msrc/backenddb/pg_lookup_mfa_challenge.c | 204++++++++++++++++---------------------------------------------------------------
Msrc/backenddb/pg_lookup_mfa_challenge.h | 18+++++++++---------
Msrc/backenddb/pg_solve_mfa_challenge.c | 28+---------------------------
Msrc/backenddb/pg_solve_mfa_challenge.h | 9+--------
Msrc/backenddb/pg_solve_mfa_challenge.sql | 36++++++++++++++++++++----------------
Msrc/include/taler_merchantdb_plugin.h | 33+++++++++++++--------------------
13 files changed, 654 insertions(+), 818 deletions(-)

diff --git a/src/backend/Makefile.am b/src/backend/Makefile.am @@ -196,6 +196,10 @@ taler_merchant_httpd_SOURCES = \ taler-merchant-httpd_private-post-transfers.h \ taler-merchant-httpd_private-post-webhooks.c \ taler-merchant-httpd_private-post-webhooks.h \ + taler-merchant-httpd_post-challenge-ID.c \ + taler-merchant-httpd_post-challenge-ID.h \ + taler-merchant-httpd_post-challenge-ID-confirm.c \ + taler-merchant-httpd_post-challenge-ID-confirm.h \ taler-merchant-httpd_post-orders-ID-abort.c \ taler-merchant-httpd_post-orders-ID-abort.h \ taler-merchant-httpd_post-orders-ID-claim.c \ diff --git a/src/backend/taler-merchant-httpd.c b/src/backend/taler-merchant-httpd.c @@ -95,6 +95,8 @@ #include "taler-merchant-httpd_private-post-token-families.h" #include "taler-merchant-httpd_private-post-transfers.h" #include "taler-merchant-httpd_private-post-webhooks.h" +#include "taler-merchant-httpd_post-challenge-ID.h" +#include "taler-merchant-httpd_post-challenge-ID-confirm.h" #include "taler-merchant-httpd_post-orders-ID-abort.h" #include "taler-merchant-httpd_post-orders-ID-claim.h" #include "taler-merchant-httpd_post-orders-ID-paid.h" @@ -125,6 +127,10 @@ char *TMH_currency; char *TMH_base_url; +char *TMH_helper_email; + +char *TMH_helper_sms; + int TMH_force_audit; struct TALER_MERCHANTDB_Plugin *TMH_db; @@ -635,7 +641,7 @@ do_shutdown (void *cls) TMH_force_gorc_resume (); TMH_force_wallet_get_order_resume (); TMH_force_wallet_refund_order_resume (); - TMH_mfa_done (); + TMH_challenge_done (); TALER_MHD_daemons_destroy (); if (NULL != instance_eh) { @@ -1011,6 +1017,7 @@ full_url_track_callback (void *cls, struct TMH_HandlerContext *hc; hc = GNUNET_new (struct TMH_HandlerContext); + hc->connection = con; GNUNET_async_scope_fresh (&hc->async_scope_id); GNUNET_SCHEDULER_begin_async_scope (&hc->async_scope_id); hc->full_url = GNUNET_strdup (full_url); @@ -1069,7 +1076,8 @@ process_basic_auth (struct TMH_HandlerContext *hc, * @return TALER_EC_NONE on success. */ static enum TALER_ErrorCode -process_bearer_auth (struct TMH_HandlerContext *hc, const char*authn_s) +process_bearer_auth (struct TMH_HandlerContext *hc, + const char *authn_s) { if (NULL == hc->instance) { @@ -1959,6 +1967,23 @@ url_handler (void *cls, .handler = &TMH_post_using_templates_ID, .max_upload = 1024 * 1024 }, + /* POST /challenge/$ID: */ + { + .url_prefix = "/challenge/", + .method = MHD_HTTP_METHOD_POST, + .have_id_segment = true, + .handler = &TMH_post_challenge_ID, + .max_upload = 1024 + }, + /* POST /challenge/$ID/confirm: */ + { + .url_prefix = "/challenge/", + .method = MHD_HTTP_METHOD_POST, + .have_id_segment = true, + .url_suffix = "confirm", + .handler = &TMH_post_challenge_ID_confirm, + .max_upload = 1024 + }, /* POST /instances */ { .url_prefix = "/instances", @@ -2859,6 +2884,31 @@ run (void *cls, return; } } + + if (GNUNET_OK != + GNUNET_CONFIGURATION_get_value_string (cfg, + "merchant", + "HELPER_SMS", + &TMH_helper_sms)) + { + GNUNET_log_config_invalid (GNUNET_ERROR_TYPE_WARNING, + "merchant", + "HELPER_SMS", + "no helper specified"); + } + + if (GNUNET_OK != + GNUNET_CONFIGURATION_get_value_string (cfg, + "merchant", + "HELPER_EMAIL", + &TMH_helper_email)) + { + GNUNET_log_config_invalid (GNUNET_ERROR_TYPE_WARNING, + "merchant", + "HELPER_EMAIL", + "no helper specified"); + } + if (GNUNET_YES == GNUNET_CONFIGURATION_get_value_yesno (cfg, "merchant", diff --git a/src/backend/taler-merchant-httpd.h b/src/backend/taler-merchant-httpd.h @@ -648,6 +648,11 @@ struct TMH_HandlerContext char *infix; /** + * Which connection was suspended. + */ + struct MHD_Connection *connection; + + /** * JSON body that was uploaded, NULL if @e has_body is false. */ json_t *request_body; @@ -725,6 +730,16 @@ extern char *TMH_currency; extern char *TMH_base_url; /** + * Name of helper program to send e-mail. + */ +extern char *TMH_helper_email; + +/** + * Name of helper program to send SMS. + */ +extern char *TMH_helper_sms; + +/** * Length of the TMH_cspecs array. */ extern unsigned int TMH_num_cspecs; diff --git a/src/backend/taler-merchant-httpd_mfa.c b/src/backend/taler-merchant-httpd_mfa.c @@ -23,607 +23,550 @@ * @author Christian Grothoff */ #include "platform.h" +#include "taler-merchant-httpd.h" #include "taler-merchant-httpd_mfa.h" -/** - * How many attempts do we allow per solution at most? Note that - * this is just for the API, the value must also match the - * database logic in create_mfa_challenge. - */ -#define MAX_SOLUTIONS 3 - /** - * How long is an OTP code valid? + * How many challenges do we allow at most per request? */ -#define OTP_TIMEOUT GNUNET_TIME_relative_multiply (GNUNET_TIME_UNIT_SECONDS, 30) - +#define MAX_CHALLENGES 4 /** - * Internal state for MFA processing. + * How long are challenges valid? */ -struct MfaState -{ - - /** - * Kept in a DLL. - */ - struct MfaState *next; +#define CHALLENGE_LIFETIME GNUNET_TIME_UNIT_DAYS - /** - * Kept in a DLL. - */ - struct MfaState *prev; - - /** - * HTTP request we are handling. - */ - struct TMH_HandlerContext *hc; - - /** - * Challenge code. - */ - char *code; - - /** - * When does @e code expire? - */ - struct GNUNET_TIME_Absolute expiration_date; - - /** - * When may we transmit a new code? - */ - struct GNUNET_TIME_Absolute retransmission_date; - - /** - * Handle to the helper process. - */ - struct GNUNET_OS_Process *child; - - /** - * Handle to wait for @e child - */ - struct GNUNET_ChildWaitHandle *cwh; - /** - * Function to call with the result. - */ - TMH_MFA_ResultCallback cb; - - /** - * Closure for @e cb. - */ - void *cb_cls; - - /** - * Address hint to return to the client. - */ - char *address_hint; - - /** - * Message to send. - */ - char *msg; - - /** - * Offset of transmission in msg. - */ - size_t msg_off; - - /** - * ID of our challenge. - */ - uint64_t challenge_id; - - /** - * Salt used when hashing the request body. - */ - struct TALER_MERCHANT_MFA_BodySalt salt; - - /** - * Salted hash over the request body. - */ - struct TALER_MERCHANT_MFA_BodyHash h_body; - - /** - * Type of the next challenge. - */ - enum TALER_MERCHANT_MFA_Channel next_challenge; - - /** - * Operation this is about. - */ - enum TALER_MERCHANT_MFA_CriticalOperation op; -}; - - -/** - * Kept in a DLL. - */ -static struct MfaState *mfa_head; - -/** - * Kept in a DLL. - */ -static struct MfaState *mfa_tail; - - -/** - * Conclude @a mfa process with status @a ret and response - * @a response - * - * @param[in] mfa MFA process to conclude and release resources for - * @param ret final result of the MFA process - * @param http_status HTTP status to give to the client if @ret is #GNUNET_NO - * @param[in] response HTTP response to return if @a ret is #GNUNET_NO - */ -static void -finish_mfa (struct MfaState *mfa, - enum GNUNET_GenericReturnValue ret, - unsigned int http_status, - struct MHD_Response *response) +enum GNUNET_GenericReturnValue +TMH_mfa_parse_challenge_id (struct TMH_HandlerContext *hc, + const char *challenge_id, + uint64_t *challenge_serial, + struct TALER_MERCHANT_MFA_BodyHash *h_body) { - GNUNET_CONTAINER_DLL_remove (mfa_head, - mfa_tail, - mfa); - mfa->cb (mfa->cb_cls, - ret, - http_status, - response); - if (NULL != mfa->cwh) + const char *dash = strchr (challenge_id, + '-'); + unsigned long long ser; + char min; + + if (NULL == dash) { - GNUNET_wait_child_cancel (mfa->cwh); - mfa->cwh = NULL; + GNUNET_break_op (0); + return (MHD_NO == + TALER_MHD_reply_with_error (hc->connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "'-' missing in challenge ID")) + ? GNUNET_SYSERR + : GNUNET_NO; } - if (NULL != mfa->child) + if ( (2 != + sscanf (challenge_id, + "%llu%c%*s", + &ser, + &min)) || + ('-' != min) ) { - (void) GNUNET_OS_process_kill (mfa->child, - SIGKILL); - GNUNET_break (GNUNET_OK == - GNUNET_OS_process_wait (mfa->child)); - mfa->child = NULL; + GNUNET_break_op (0); + return (MHD_NO == + TALER_MHD_reply_with_error (hc->connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "Invalid number for challenge ID")) + ? GNUNET_SYSERR + : GNUNET_NO; } - GNUNET_free (mfa->msg); - GNUNET_free (mfa->code); - GNUNET_free (mfa->address_hint); - GNUNET_free (mfa); -} - - -void -TMH_mfa_done () -{ - struct MfaState *mfa; - - while (NULL != (mfa = mfa_head)) + if (GNUNET_OK != + GNUNET_STRINGS_string_to_data (dash + 1, + strlen (dash) + 1, + h_body, + sizeof (*h_body))) { - finish_mfa (mfa, - GNUNET_SYSERR, - 0, - NULL); + GNUNET_break_op (0); + return (MHD_NO == + TALER_MHD_reply_with_error (hc->connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "Malformed challenge ID")) + ? GNUNET_SYSERR + : GNUNET_NO; } + *challenge_serial = (uint64_t) ser; + return GNUNET_OK; } /** - * Obtain hint about the @a target_address of type @a channel to - * return to the client. + * Check if the given authentication check was already completed. * - * @param channel type of challenge - * @param target_address address we will sent the challenge to - * @return hint for the user about the address + * @param[in,out] hc handler context of the connection to authorize + * @param op operation for which we are requiring authorization + * @param challenge_id ID of the challenge to check if it is done + * @param[out] solved set to true if the challenge was solved, + * set to false if @a challenge_id was not found + * @param[out] channel TAN channel that was used, + * set to #TALER_MERCHANT_MFA_CHANNEL_NONE if @a challenge_id + * was not found + * @param[out] target_address address which was validated, + * set to NULL if @a challenge_id was not found + * @param[out] retry_counter how many attempts are left on the challenge + * @return #GNUNET_OK on success (challenge found) + * #GNUNET_NO if an error message was returned to the client + * #GNUNET_SYSERR to just close the connection */ -static char * -get_hint (enum TALER_MERCHANT_MFA_Channel channel, - const char *target_address) +static enum GNUNET_GenericReturnValue +mfa_challenge_check ( + struct TMH_HandlerContext *hc, + enum TALER_MERCHANT_MFA_CriticalOperation op, + const char *challenge_id, + bool *solved, + enum TALER_MERCHANT_MFA_Channel *channel, + char **target_address, + uint32_t *retry_counter) { - switch (channel) + uint64_t challenge_serial; + struct TALER_MERCHANT_MFA_BodyHash h_body; + struct TALER_MERCHANT_MFA_BodyHash x_h_body; + struct TALER_MERCHANT_MFA_BodySalt salt; + struct GNUNET_TIME_Absolute retransmission_date; + enum TALER_MERCHANT_MFA_CriticalOperation xop; + enum GNUNET_DB_QueryStatus qs; + struct GNUNET_TIME_Absolute confirmation_date; + enum GNUNET_GenericReturnValue ret; + + ret = TMH_mfa_parse_challenge_id (hc, + challenge_id, + &challenge_serial, + &h_body); + if (GNUNET_OK != ret) + return ret; + *target_address = NULL; + *solved = false; + *channel = TALER_MERCHANT_MFA_CHANNEL_NONE; + *retry_counter = UINT_MAX; + qs = TMH_db->lookup_mfa_challenge (TMH_db->cls, + hc->instance->settings.id, + challenge_serial, + &x_h_body, + &salt, + target_address, + &xop, + &confirmation_date, + &retransmission_date, + retry_counter, + channel); + switch (qs) { - case TALER_MERCHANT_MFA_CHANNEL_NONE: - GNUNET_assert (0); - return NULL; - case TALER_MERCHANT_MFA_CHANNEL_SMS: - { - size_t slen = strlen (target_address); - const char *end; - - if (slen > 4) - end = &target_address[slen - 4]; - else - end = &target_address[slen / 2]; - return GNUNET_strdup (end); - } - case TALER_MERCHANT_MFA_CHANNEL_EMAIL: - { - const char *at; - size_t len; - - at = strchr (target_address, - '@'); - if (NULL == at) - len = 0; - else - len = at - target_address; - return GNUNET_strndup (target_address, - len); - } - case TALER_MERCHANT_MFA_CHANNEL_TOTP: + case GNUNET_DB_STATUS_HARD_ERROR: + GNUNET_break (0); + return (MHD_NO == + TALER_MHD_reply_with_error (hc->connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_COMMIT_FAILED, + NULL)) + ? GNUNET_SYSERR + : GNUNET_NO; + case GNUNET_DB_STATUS_SOFT_ERROR: GNUNET_break (0); - return GNUNET_strdup ("TOTP is not implemented: #10327"); + return (MHD_NO == + TALER_MHD_reply_with_error (hc->connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_SOFT_FAILURE, + NULL)) + ? GNUNET_SYSERR + : GNUNET_NO; + case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: + return GNUNET_OK; + case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: + break; } - GNUNET_break (0); - return NULL; -} - -/** - * Generate an error for @a mfa. - * - * @param[in] mfa process to generate an error response for - * @param http_status HTTP status of the response - * @param ec Taler error code to return - * @param hint hint to return, can be NULL - */ -static void -respond_with_error (struct MfaState *mfa, - unsigned int http_status, - enum TALER_ErrorCode ec, - const char *hint) -{ - finish_mfa (mfa, - GNUNET_NO, - http_status, - TALER_MHD_make_error (ec, - hint)); + if (xop != op) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Challenge was for a different operation (%d!=%d)!\n", + (int) op, + (int) xop); + *solved = false; + return GNUNET_OK; + } + TALER_MERCHANT_mfa_body_hash (hc->request_body, + &salt, + &h_body); + if (0 != + GNUNET_memcmp (&h_body, + &x_h_body)) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Challenge was for a different request body!\n"); + *solved = false; + return GNUNET_OK; + } + *solved = (! GNUNET_TIME_absolute_is_future (confirmation_date)); + return GNUNET_OK; } /** - * Generate a response to the client pointing them to the challenge - * that was sent. + * Multi-factor authentication check to see if for the given @a instance_id + * and the @a op operation all the TAN channels given in @a required_tans have + * been satisfied. Note that we always satisfy @a required_tans in the order + * given in the array, so if the last one is satisfied, all previous ones must + * have been satisfied before. * - * @param[in] mfa process to generate response for - */ -static void -respond_with_challenge (struct MfaState *mfa) -{ - finish_mfa ( - mfa, - GNUNET_NO, - MHD_HTTP_FORBIDDEN, /* FIXME: check HTTP status code! Maybe use ACCEPTED? */ - TALER_MHD_MAKE_JSON_PACK ( - GNUNET_JSON_pack_string ("address_hint", - mfa->address_hint), - GNUNET_JSON_pack_string ("challenge_type", - TALER_MERCHANT_MFA_channel_to_string ( - mfa->next_challenge)), - GNUNET_JSON_pack_uint64 ("challenge_id", - mfa->challenge_id), - GNUNET_JSON_pack_data_auto ("h_body", - &mfa->h_body))); -} - - -/** - * Function called when our SMS helper has terminated. + * If the challenges has not been satisfied, an appropriate response + * is returned to the client of @a hc. * - * @param cls our `struct ANASTASIS_AUHTORIZATION_State` - * @param type type of the process - * @param exit_code status code of the process + * @param[in,out] hc handler context of the connection to authorize + * @param op operation for which we are performing + * @param channel TAN channel to try + * @param expiration_date when should the challenge expire + * @param required_address addresses to use for + * the respective challenge + * @param[out] challenge_id set to the challenge ID, to be freed by + * the caller + * @return #GNUNET_OK on success, + * #GNUNET_NO if an error message was returned to the client + * #GNUNET_SYSERR to just close the connection */ -static void -transmission_done_cb (void *cls, - enum GNUNET_OS_ProcessStatusType type, - long unsigned int exit_code) +static enum GNUNET_GenericReturnValue +mfa_challenge_start ( + struct TMH_HandlerContext *hc, + enum TALER_MERCHANT_MFA_CriticalOperation op, + enum TALER_MERCHANT_MFA_Channel channel, + struct GNUNET_TIME_Absolute expiration_date, + const char *required_address, + char **challenge_id) { - struct MfaState *mfa = cls; enum GNUNET_DB_QueryStatus qs; - - mfa->cwh = NULL; - if (NULL != mfa->child) - { - GNUNET_OS_process_destroy (mfa->child); - mfa->child = NULL; - } - if (0 == mfa->challenge_id) - { - /* 0: fresh challenge, must create one */ - qs = TMH_db->create_mfa_challenge (TMH_db->cls, - mfa->hc->instance->settings.id, - mfa->op, - &mfa->h_body, - &mfa->salt, - mfa->code, - mfa->expiration_date, - mfa->retransmission_date, - mfa->next_challenge, - mfa->address_hint, - &mfa->challenge_id); - } - else - { - qs = TMH_db->update_mfa_challenge (TMH_db->cls, - mfa->challenge_id, - mfa->code, - MAX_SOLUTIONS, - mfa->expiration_date, - mfa->retransmission_date); - } + struct TALER_MERCHANT_MFA_BodySalt salt; + struct TALER_MERCHANT_MFA_BodyHash h_body; + uint64_t challenge_serial; + + GNUNET_CRYPTO_random_block (GNUNET_CRYPTO_QUALITY_NONCE, + &salt, + sizeof (salt)); + TALER_MERCHANT_mfa_body_hash (hc->request_body, + &salt, + &h_body); + qs = TMH_db->create_mfa_challenge (TMH_db->cls, + hc->instance->settings.id, + op, + &h_body, + &salt, + NULL, + expiration_date, + GNUNET_TIME_UNIT_ZERO_ABS, + channel, + required_address, + &challenge_serial); switch (qs) { case GNUNET_DB_STATUS_HARD_ERROR: GNUNET_break (0); - respond_with_error (mfa, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_COMMIT_FAILED, - NULL); - return; + return (MHD_NO == + TALER_MHD_reply_with_error (hc->connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_COMMIT_FAILED, + NULL)) + ? GNUNET_SYSERR + : GNUNET_NO; case GNUNET_DB_STATUS_SOFT_ERROR: GNUNET_break (0); - respond_with_error (mfa, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_SOFT_FAILURE, - NULL); - return; + return (MHD_NO == + TALER_MHD_reply_with_error (hc->connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_SOFT_FAILURE, + NULL)) + ? GNUNET_SYSERR + : GNUNET_NO; case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: - GNUNET_break (0); - respond_with_error (mfa, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_INVARIANT_FAILURE, - "no results on INSERT, but success?"); - return; + GNUNET_assert (0); + break; case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: break; } - respond_with_challenge (mfa); + { + char *h_body_s; + + h_body_s = GNUNET_STRINGS_data_to_string_alloc (&h_body, + sizeof (h_body)); + GNUNET_asprintf (challenge_id, + "%llu-%s", + (unsigned long long) challenge_serial, + h_body_s); + GNUNET_free (h_body_s); + } + return GNUNET_OK; } /** - * Setup challenge code for @a mfa and send it to the - * @a required_address; on success. + * Internal book-keeping for #TMH_mfa_challenges_do(). + */ +struct Challenge +{ + /** + * Channel on which the challenge is transmitted. + */ + enum TALER_MERCHANT_MFA_Channel channel; + + /** + * Address to send the challenge to. + */ + const char *required_address; + + /** + * Internal challenge ID. + */ + char *challenge_id; + + /** + * True if the challenge was solved. + */ + bool solved; + + /** + * True if the challenge could still be solved. + */ + bool solvable; + +}; + + +/** + * Check that a set of MFA challenges has been satisfied by the + * client for the request in @a hc. * - * @param[in,out] mfa process to send the challenge for - * @param required_address where to send the challenge + * @param[in,out] hc handler context with the connection to the client + * @param op operation for which we should check challenges for + * @param combi_and true to tell the client to solve all challenges (AND), + * false means that any of the challenges will do (OR) + * @param ... pairs of channel and address, terminated by + * #TALER_MERCHANT_MFA_CHANNEL_NONE + * @return #GNUNET_OK on success (challenges satisfied) + * #GNUNET_NO if an error message was returned to the client + * #GNUNET_SYSERR to just close the connection */ -static void -send_challenge (struct MfaState *mfa, - const char *required_address) +enum GNUNET_GenericReturnValue +TMH_mfa_challenges_do ( + struct TMH_HandlerContext *hc, + enum TALER_MERCHANT_MFA_CriticalOperation op, + bool combi_and, + ...) { - const char *prog; + struct Challenge challenges[MAX_CHALLENGES]; + const char *challenge_ids[MAX_CHALLENGES]; + size_t num_challenges; + char *challenge_ids_copy = NULL; + size_t num_provided_challenges; + enum GNUNET_GenericReturnValue ret; - switch (mfa->next_challenge) - { - case TALER_MERCHANT_MFA_CHANNEL_NONE: - GNUNET_assert (0); - break; - case TALER_MERCHANT_MFA_CHANNEL_SMS: - mfa->expiration_date - = GNUNET_TIME_relative_to_absolute (GNUNET_TIME_UNIT_HOURS); - mfa->retransmission_date - = GNUNET_TIME_relative_to_absolute (GNUNET_TIME_UNIT_HOURS); - GNUNET_asprintf (&mfa->code, - "%llu", - (unsigned long long) - GNUNET_CRYPTO_random_u64 (GNUNET_CRYPTO_QUALITY_NONCE, - 100000000)); - prog = "taler-merchant-helper-send-sms"; - break; - case TALER_MERCHANT_MFA_CHANNEL_EMAIL: - mfa->expiration_date - = GNUNET_TIME_relative_to_absolute (GNUNET_TIME_UNIT_HOURS); - mfa->retransmission_date - = GNUNET_TIME_relative_to_absolute (GNUNET_TIME_UNIT_HOURS); - GNUNET_asprintf (&mfa->code, - "%llu", - (unsigned long long) - GNUNET_CRYPTO_random_u64 (GNUNET_CRYPTO_QUALITY_NONCE, - 100000000)); - prog = "taler-merchant-helper-send-email"; - break; - case TALER_MERCHANT_MFA_CHANNEL_TOTP: - mfa->expiration_date - = GNUNET_TIME_relative_to_absolute (OTP_TIMEOUT); - mfa->retransmission_date - = GNUNET_TIME_relative_to_absolute (OTP_TIMEOUT); - respond_with_error (mfa, - MHD_HTTP_NOT_IMPLEMENTED, - TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE, - "#10327"); - return; - } { - /* Start child process and feed pipe */ - struct GNUNET_DISK_PipeHandle *p; - struct GNUNET_DISK_FileHandle *pipe_stdin; + va_list ap; - p = GNUNET_DISK_pipe (GNUNET_DISK_PF_BLOCKING_RW); - if (NULL == p) - { - respond_with_error (mfa, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE, - "pipe"); - return; - } - mfa->child = GNUNET_OS_start_process (GNUNET_OS_INHERIT_STD_ERR, - p, - NULL, - NULL, - prog, - prog, - required_address, - NULL); - if (NULL == mfa->child) + va_start (ap, + combi_and); + for (num_challenges = 0; + num_challenges < MAX_CHALLENGES; + num_challenges++) { - GNUNET_DISK_pipe_close (p); - respond_with_error (mfa, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_ANASTASIS_SMS_HELPER_EXEC_FAILED, - "exec"); - return; + enum TALER_MERCHANT_MFA_Channel channel; + const char *address; + + channel = va_arg (ap, + enum TALER_MERCHANT_MFA_Channel); + if (TALER_MERCHANT_MFA_CHANNEL_NONE == channel) + break; + address = va_arg (ap, + const char *); + GNUNET_assert (NULL != address); + challenges[num_challenges].channel = channel; + challenges[num_challenges].required_address = address; + challenges[num_challenges].challenge_id = NULL; + challenges[num_challenges].solved = false; + challenges[num_challenges].solvable = true; } + va_end (ap); + } + + if (0 == num_challenges) + { + /* No challenges required. Strange... */ + return GNUNET_OK; + } - pipe_stdin = GNUNET_DISK_pipe_detach_end (p, - GNUNET_DISK_PIPE_END_WRITE); - GNUNET_assert (NULL != pipe_stdin); - GNUNET_DISK_pipe_close (p); - GNUNET_asprintf (&mfa->msg, - "%s\nTaler-Merchant:\n%s", - mfa->code, - mfa->hc->instance->settings.id); + { + const char *challenge_ids_header; + + challenge_ids_header + = MHD_lookup_connection_value (hc->connection, + MHD_HEADER_KIND, + "Taler-Challenge-Ids"); + num_provided_challenges = 0; + if (NULL != challenge_ids_header) { - const char *off = mfa->msg; - size_t left = strlen (off); + challenge_ids_copy = GNUNET_strdup (challenge_ids_header); - while (0 != left) + for (char *token = strtok (challenge_ids_copy, + ","); + NULL != token; + token = strtok (NULL, + ",")) { - ssize_t ret; - - ret = GNUNET_DISK_file_write (pipe_stdin, - off, - left); - if (ret <= 0) + if (num_provided_challenges > MAX_CHALLENGES) { - respond_with_error (mfa, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_ANASTASIS_SMS_HELPER_EXEC_FAILED, - "write"); - return; + GNUNET_break_op (0); + GNUNET_free (challenge_ids_copy); + return (MHD_NO == + TALER_MHD_reply_with_error ( + hc->connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_HTTP_HEADERS_MALFORMED, + "Taler-Challenge-Ids")) + ? GNUNET_SYSERR + : GNUNET_NO; } - mfa->msg_off += ret; - off += ret; - left -= ret; + challenge_ids[num_provided_challenges] = token; + num_provided_challenges++; } - GNUNET_DISK_file_close (pipe_stdin); } } - mfa->cwh = GNUNET_wait_child (mfa->child, - &transmission_done_cb, - mfa); -} - - -void -TMH_mfa_challenge_check ( - struct TMH_HandlerContext *hc, - enum TALER_MERCHANT_MFA_CriticalOperation op, - enum TALER_MERCHANT_MFA_Channel required_tans[], - const char *required_addresses[], - TMH_MFA_ResultCallback cb, - void *cb_cls) -{ - struct MfaState *mfa; - enum TALER_MERCHANT_MFA_Channel last_channel; - uint32_t retry_counter; - enum GNUNET_DB_QueryStatus qs; - struct GNUNET_TIME_Absolute confirmation_date; - mfa = GNUNET_new (struct MfaState); - mfa->hc = hc; - mfa->cb = cb; - mfa->cb_cls = cb_cls; - mfa->op = op; - GNUNET_CONTAINER_DLL_insert (mfa_head, - mfa_tail, - mfa); - if (TALER_MERCHANT_MFA_CHANNEL_NONE == required_tans[0]) + /* Check provided challenges against requirements */ + for (size_t i = 0; i < num_provided_challenges; i++) { - finish_mfa (mfa, - GNUNET_OK, - 0, - NULL); - return; + bool solved; + enum TALER_MERCHANT_MFA_Channel channel; + char *target_address; + uint32_t retry_counter; + + ret = mfa_challenge_check (hc, + op, + challenge_ids[i], + &solved, + &channel, + &target_address, + &retry_counter); + if (GNUNET_OK != ret) + goto cleanup; + for (size_t j = 0; j < num_challenges; j++) + { + if ( (challenges[j].channel == channel) && + (NULL == challenges[j].challenge_id) && + (NULL != target_address /* just to be sure */) && + (0 == strcmp (target_address, + challenges[j].required_address) ) ) + { + challenges[j].solved + = solved; + challenges[j].challenge_id + = GNUNET_strdup (challenge_ids[i]); + if ( (! solved) && + (0 == retry_counter) ) + { + /* can't be solved anymore! */ + challenges[i].solvable = false; + } + break; + } + } + GNUNET_free (target_address); } - qs = TMH_db->lookup_mfa_challenge (TMH_db->cls, - hc->instance->settings.id, - op, - hc->request_body, - &mfa->h_body, - &mfa->salt, - &confirmation_date, - &mfa->retransmission_date, - &retry_counter, - &last_channel, - &mfa->challenge_id); - switch (qs) + { - case GNUNET_DB_STATUS_HARD_ERROR: - GNUNET_break (0); - respond_with_error (mfa, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_COMMIT_FAILED, - NULL); - return; - case GNUNET_DB_STATUS_SOFT_ERROR: - GNUNET_break (0); - respond_with_error (mfa, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_SOFT_FAILURE, - NULL); - return; - case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: - GNUNET_CRYPTO_random_block (GNUNET_CRYPTO_QUALITY_NONCE, - &mfa->salt, - sizeof (mfa->salt)); - TALER_MERCHANT_mfa_body_hash (hc->request_body, - &mfa->salt, - &mfa->h_body); - mfa->next_challenge = required_tans[0]; - mfa->address_hint = get_hint (mfa->next_challenge, - required_addresses[0]); - send_challenge (mfa, - required_addresses[0]); - return; - case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: - break; + struct GNUNET_TIME_Absolute expiration_date + = GNUNET_TIME_relative_to_absolute (CHALLENGE_LIFETIME); + + /* Start new challenges for unsolved requirements */ + for (size_t i = 0; i < num_challenges; i++) + { + if (NULL == challenges[i].challenge_id) + { + GNUNET_assert (! challenges[i].solved); + GNUNET_assert (challenges[i].solvable); + ret = mfa_challenge_start (hc, + op, + challenges[i].channel, + expiration_date, + challenges[i].required_address, + &challenges[i].challenge_id); + if (GNUNET_OK != ret) + goto cleanup; + } + } } - for (unsigned int i = 0; - TALER_MERCHANT_MFA_CHANNEL_NONE != required_tans[i]; - i++) + { - if (last_channel == required_tans[i]) + bool all_solved = true; + bool any_solved = false; + bool solvable = true; + + for (size_t i = 0; i < num_challenges; i++) { - if (GNUNET_TIME_absolute_is_past (confirmation_date)) + if (challenges[i].solved) { - /* Last challenge WAS solved, move on! */ - mfa->next_challenge = required_tans[i + 1]; - if (TALER_MERCHANT_MFA_CHANNEL_NONE == mfa->next_challenge) - { - finish_mfa (mfa, - GNUNET_OK, - 0, - NULL); - return; - } - mfa->address_hint = get_hint (mfa->next_challenge, - required_addresses[i + 1]); - send_challenge (mfa, - required_addresses[i + 1]); - return; + any_solved = true; } else { - /* challenge was not yet solved, possibly re-transmit - a new one */ - mfa->next_challenge = last_channel; - mfa->address_hint = get_hint (mfa->next_challenge, - required_addresses[i]); - if (GNUNET_TIME_absolute_is_past (mfa->retransmission_date)) - { - send_challenge (mfa, - required_addresses[i]); - return; - } - respond_with_challenge (mfa); - return; + all_solved = false; + if (combi_and && + (! challenges[i].solvable) ) + solvable = false; } } + + if ( (combi_and && all_solved) || + (! combi_and && any_solved) ) + { + /* Authorization successful */ + ret = GNUNET_OK; + goto cleanup; + } + if (! solvable) + { + ret = (MHD_NO == + TALER_MHD_reply_with_error ( + hc->connection, + MHD_HTTP_FORBIDDEN, + TALER_EC_MERCHANT_MFA_FORBIDDEN, + GNUNET_TIME_relative2s (CHALLENGE_LIFETIME, + false))) + ? GNUNET_SYSERR + : GNUNET_NO; + goto cleanup; + } } - /* challenge was satisfied that is not in our list! */ - GNUNET_break (0); - respond_with_error (mfa, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_INVARIANT_FAILURE, - "unexpected challenge already satisfied"); + + /* Return challenges to client */ + { + json_t *jchallenges; + + jchallenges = json_array (); + GNUNET_assert (NULL != jchallenges); + for (size_t i = 0; i<num_challenges; i++) + { + GNUNET_assert (0 == + json_array_append_new ( + jchallenges, + json_string (challenges[i].challenge_id))); + } + ret = (MHD_NO == + TALER_MHD_REPLY_JSON_PACK ( + hc->connection, + MHD_HTTP_ACCEPTED, + GNUNET_JSON_pack_bool ("combi_and", + combi_and), + GNUNET_JSON_pack_array_steal ("challenges", + jchallenges))) + ? GNUNET_SYSERR + : GNUNET_NO; + } + +cleanup: + for (size_t i = 0; i < num_challenges; i++) + GNUNET_free (challenges[i].challenge_id); + GNUNET_free (challenge_ids_copy); + return ret; } diff --git a/src/backend/taler-merchant-httpd_mfa.h b/src/backend/taler-merchant-httpd_mfa.h @@ -30,59 +30,43 @@ /** - * Function called with the result of an MFA operation. + * Parse the given @a challenge_id into its parts. * - * @param cls closure - * @param res #GNUNET_OK if all challenges have been satisfied - * #GNUNET_NO if a challenge response was returned to the client - * #GNUNET_SYSERR to just close the connection without response - * @param http_status HTTP status code of @a response, non-zero only if - * @a res is #GNUNET_NO - * @param response HTTP response to queue, only if @a res is #GNUNET_NO + * @param[in,out] hc handler context with the connection to the client + * @param challenge_id challenge of "$NUMBER-$H_BODY" + * @param[out] challenge_serial set to $NUMBER + * @param[out] h_body set to $H_BODY (decoded) + * @return #GNUNET_OK on success, + * #GNUNET_NO if an error message was returned to the client + * #GNUNET_SYSERR to just close the connection */ -typedef void -(*TMH_MFA_ResultCallback)(void *cls, - enum GNUNET_GenericReturnValue res, - unsigned int http_status, - struct MHD_Response *response); +enum GNUNET_GenericReturnValue +TMH_mfa_parse_challenge_id (struct TMH_HandlerContext *hc, + const char *challenge_id, + uint64_t *challenge_serial, + struct TALER_MERCHANT_MFA_BodyHash *h_body); /** - * Multi-factor authentication check to see if for the given @a instance_id - * and the @a op operation all the TAN channels given in @a required_tans have - * been satisfied. Note that we always satisfy @a required_tans in the order - * given in the array, so if the last one is satisfied, all previous ones must - * have been satisfied before. + * Check that a set of MFA challenges has been satisfied by the + * client for the request in @a hc. * - * If the challenges has not been satisfied, an appropriate response - * is returned to the client of @a hc. - * - * @param[in,out] hc handler context of the connection to authorize - * @param op operation for which we are performing - * @param required_tans array of TAN channels to try, - * terminated with #TALER_MERCHANT_MFA_CHANNEL_NONE - * @param required_addresses array of addresses to use for - * the respective challenge, NULL-terminated, same length - * as @a required_tans - * @param cb continuation to call with the result - * @param cb_cls closure for @a cb + * @param[in,out] hc handler context with the connection to the client + * @param op operation for which we should check challenges for + * @param combi_and true to tell the client to solve all challenges (AND), + * false means that any of the challenges will do (OR) + * @param ... pairs of channel and address, terminated by + * #TALER_MERCHANT_MFA_CHANNEL_NONE + * @return #GNUNET_OK on success (challenges satisfied) + * #GNUNET_NO if an error message was returned to the client + * #GNUNET_SYSERR to just close the connection */ -void -TMH_mfa_challenge_check ( +enum GNUNET_GenericReturnValue +TMH_mfa_challenges_do ( struct TMH_HandlerContext *hc, enum TALER_MERCHANT_MFA_CriticalOperation op, - enum TALER_MERCHANT_MFA_Channel required_tans[], - const char *required_addresses[], - TMH_MFA_ResultCallback cb, - void *cb_cls); - - -/** - * Function to call when the HTTP server is shutting down to - * clean up all ongoing MFA challenge processes. - */ -void -TMH_mfa_done (void); + bool combi_and, + ...); #endif diff --git a/src/backenddb/pg_create_mfa_challenge.c b/src/backenddb/pg_create_mfa_challenge.c @@ -37,7 +37,7 @@ TMH_PG_create_mfa_challenge ( struct GNUNET_TIME_Absolute expiration_date, struct GNUNET_TIME_Absolute retransmission_date, enum TALER_MERCHANT_MFA_Channel tan_channel, - const char *tan_info, + const char *required_address, uint64_t *challenge_id) { struct PostgresClosure *pg = cls; @@ -54,9 +54,7 @@ TMH_PG_create_mfa_challenge ( GNUNET_PQ_query_param_absolute_time (&retransmission_date), GNUNET_PQ_query_param_string (instance_id), GNUNET_PQ_query_param_string (channel_str), - (NULL == tan_info) - ? GNUNET_PQ_query_param_null () - : GNUNET_PQ_query_param_string (tan_info), /* $10 */ + GNUNET_PQ_query_param_string (required_address), /* $10 */ GNUNET_PQ_query_param_end }; struct GNUNET_PQ_ResultSpec rs[] = { @@ -78,7 +76,7 @@ TMH_PG_create_mfa_challenge ( " ,retry_counter" " ,merchant_serial" " ,tan_channel" - " ,tan_info)" + " ,required_address)" " SELECT" " $1, $2, $3, $4, $5, $6, $7, 3, merchant_serial, $9, $10" " FROM merchant_instances" diff --git a/src/backenddb/pg_create_mfa_challenge.h b/src/backenddb/pg_create_mfa_challenge.h @@ -39,8 +39,8 @@ * @param retansmission_date when do we next allow retransmission * of the challenge * @param tan_channel which channel was used - * @param tan_info information message to give to the user - * (about where the challenge was sent), can be NULL + * @param required_address address + * where the challenge is to be sent * @param[out] challenge_id set to the ID of the new challenge * @return database result code */ @@ -55,7 +55,7 @@ TMH_PG_create_mfa_challenge ( struct GNUNET_TIME_Absolute expiration_date, struct GNUNET_TIME_Absolute retransmission_date, enum TALER_MERCHANT_MFA_Channel tan_channel, - const char *tan_info, + const char *required_address, uint64_t *challenge_id); #endif diff --git a/src/backenddb/pg_lookup_mfa_challenge.c b/src/backenddb/pg_lookup_mfa_challenge.c @@ -25,204 +25,82 @@ #include "pg_lookup_mfa_challenge.h" #include "pg_helper.h" -/** - * Closure for #handle_mfa_challenge(). - */ -struct LookupChallengeContext -{ - /** - * Provides the body to check against. - */ - const json_t *body; - - /** - * Set to the resulting salted hash of @e body. - */ - struct TALER_MERCHANT_MFA_BodyHash *h_body; - - /** - * Set to the salt used to hash @e body. - */ - struct TALER_MERCHANT_MFA_BodySalt *salt; - - /** - * Set to when the challenge we completed. - */ - struct GNUNET_TIME_Absolute *confirmation_date; - - /** - * Set to the next possible retransmission date for the challenge. - */ - struct GNUNET_TIME_Absolute *retransmission_date; - - /** - * Set to the remaining retry counter for this challenge. - */ - uint32_t *retry_counter; - - /** - * Set to the channel used to transmit this challenge. - */ - enum TALER_MERCHANT_MFA_Channel *tan_channel; - - /** - * Set to the unique ID of the challenge in our backend. - */ - uint64_t *challenge_id; - - /** - * Set to true if a body matched and any of the other fields were - * initialized. - */ - bool body_matched; - - /** - * Set to true on errors. - */ - bool extract_failed; -}; - - -/** - * Function to be called with the results of a SELECT statement - * that has returned @a num_results results about products. - * - * @param[in,out] cls of type `struct LookupChallengeContext *` - * @param result the postgres result - * @param num_results the number of results in @a result - */ -static void -handle_mfa_challenge (void *cls, - PGresult *result, - unsigned int num_results) -{ - struct LookupChallengeContext *ctx = cls; - - for (unsigned int i = 0; i < num_results; i++) - { - struct TALER_MERCHANT_MFA_BodyHash h_body; - struct TALER_MERCHANT_MFA_BodySalt salt; - char *channel_str = NULL; - struct GNUNET_PQ_ResultSpec rs[] = { - GNUNET_PQ_result_spec_auto_from_type ("h_body", - &h_body), - GNUNET_PQ_result_spec_auto_from_type ("salt", - &salt), - GNUNET_PQ_result_spec_absolute_time ("confirmation_date", - ctx->confirmation_date), - GNUNET_PQ_result_spec_absolute_time ("retransmission_date", - ctx->retransmission_date), - GNUNET_PQ_result_spec_uint32 ("retry_counter", - ctx->retry_counter), - GNUNET_PQ_result_spec_allow_null ( - GNUNET_PQ_result_spec_string ("tan_channel", - &channel_str), - NULL), - GNUNET_PQ_result_spec_uint64 ("challenge_id", - ctx->challenge_id), - GNUNET_PQ_result_spec_end - }; - struct TALER_MERCHANT_MFA_BodyHash h2; - - if (GNUNET_OK != - GNUNET_PQ_extract_result (result, - rs, - i)) - { - GNUNET_break (0); - ctx->extract_failed = true; - return; - } - /* Note: missed optimization: only serialize and - hash body once, only add salt each time... */ - TALER_MERCHANT_mfa_body_hash (ctx->body, - &salt, - &h2); - if (0 == GNUNET_memcmp (&h2, - &h_body)) - { - ctx->body_matched = true; - *ctx->tan_channel = TALER_MERCHANT_MFA_channel_from_string (channel_str); - *ctx->h_body = h2; - *ctx->salt = salt; - } - GNUNET_PQ_cleanup_result (rs); - if (ctx->body_matched) - break; - } -} - enum GNUNET_DB_QueryStatus TMH_PG_lookup_mfa_challenge ( void *cls, const char *instance_id, - enum TALER_MERCHANT_MFA_CriticalOperation op, - const json_t *body, - struct TALER_MERCHANT_MFA_BodyHash *h_body, + uint64_t challenge_id, + const struct TALER_MERCHANT_MFA_BodyHash *h_body, struct TALER_MERCHANT_MFA_BodySalt *salt, + char **required_address, + enum TALER_MERCHANT_MFA_CriticalOperation *op, struct GNUNET_TIME_Absolute *confirmation_date, struct GNUNET_TIME_Absolute *retransmission_date, uint32_t *retry_counter, - enum TALER_MERCHANT_MFA_Channel *tan_channel, - uint64_t *challenge_id) + enum TALER_MERCHANT_MFA_Channel *tan_channel) { struct PostgresClosure *pg = cls; - struct LookupChallengeContext ctx = { - .body = body, - .h_body = h_body, - .salt = salt, - .confirmation_date = confirmation_date, - .retransmission_date = retransmission_date, - .retry_counter = retry_counter, - .tan_channel = tan_channel, - .challenge_id = challenge_id - }; - const char *op_str = TALER_MERCHANT_MFA_co_to_string (op); struct GNUNET_PQ_QueryParam params[] = { GNUNET_PQ_query_param_string (instance_id), - GNUNET_PQ_query_param_string (op_str), + GNUNET_PQ_query_param_uint64 (&challenge_id), + GNUNET_PQ_query_param_auto_from_type (h_body), GNUNET_PQ_query_param_end }; + char *op_str; + char *chan_str; + struct GNUNET_PQ_ResultSpec rs[] = { + GNUNET_PQ_result_spec_string ("op", + &op_str), + GNUNET_PQ_result_spec_auto_from_type ("salt", + salt), + GNUNET_PQ_result_spec_absolute_time ("confirmation_date", + confirmation_date), + GNUNET_PQ_result_spec_absolute_time ("retransmission_date", + retransmission_date), + GNUNET_PQ_result_spec_uint32 ("retry_counter", + retry_counter), + GNUNET_PQ_result_spec_string ("tan_channel", + &chan_str), + GNUNET_PQ_result_spec_string ("required_address", + required_address), + GNUNET_PQ_result_spec_end + }; enum GNUNET_DB_QueryStatus qs; PREPARE (pg, "lookup_mfa_challenge", "SELECT " - " h_body" + " op::TEXT" " ,salt" " ,confirmation_date" " ,retransmission_date" " ,retry_counter" - " ,tan_channel" - " ,challenge_id" + " ,required_address" + " ,tan_channel::TEXT" " FROM tan_challenges" " WHERE merchant_serial IN" " (SELECT merchant_serial" " FROM merchant_instances" " WHERE merchant_id = $1)" - " AND (tc.op = $2)" + " AND (tc.challenge_id = $2)" + " AND (tc.h_body = $3)" " AND (expiration_date / 1000 / 1000" " > EXTRACT(EPOCH FROM NOW()" - " AT TIME ZONE 'UTC')::bigint)" - " ORDER BY creation_date DESC"); + " AT TIME ZONE 'UTC')::bigint)"); /* Initialize to conservative values in case qs ends up <= 0 */ *tan_channel = TALER_MERCHANT_MFA_CHANNEL_NONE; + *op = TALER_MERCHANT_MFA_CO_NONE; *retry_counter = 0; - *challenge_id = 0; - qs = GNUNET_PQ_eval_prepared_multi_select (pg->conn, - "lookup_mfa_challenge", - params, - &handle_mfa_challenge, - &ctx); + qs = GNUNET_PQ_eval_prepared_singleton_select (pg->conn, + "lookup_mfa_challenge", + params, + rs); if (qs <= 0) return qs; - if (ctx.extract_failed) - { - GNUNET_break (0); - return GNUNET_DB_STATUS_HARD_ERROR; - } - if (! ctx.body_matched) - return GNUNET_DB_STATUS_SUCCESS_NO_RESULTS; + *tan_channel = TALER_MERCHANT_MFA_channel_from_string (chan_str); + *op = TALER_MERCHANT_MFA_co_from_string (op_str); + GNUNET_free (chan_str); + GNUNET_free (op_str); return qs; } diff --git a/src/backenddb/pg_lookup_mfa_challenge.h b/src/backenddb/pg_lookup_mfa_challenge.h @@ -32,10 +32,11 @@ * * @param cls closure * @param instance_id instance for which the challenge is being looked up - * @param op operation that triggered the MFA request - * @param body body to match - * @param[out] h_body hash of the request body + * @param challenge_id set to the ID of the challenge + * @param h_body hash of the request body * @param[out] salt salt used to compute @a h_body + * @param[out] required address set to where the challenge is to be send + * @param[out] op operation that triggered the MFA request * @param[out] confirmation_date when was the challenge solved, * set to "GNUNET_TIME_ABSOLUTE_NEVER" if unsolved * @param[out] retransmission_date set to when a fresh challenge @@ -43,22 +44,21 @@ * @param[out] retry_counter set to the number of attempts that remain * for solving the challenge (after this time) * @param[out] tan_channel which channel was used - * @param[out] challenge_id set to the ID of the new challenge * @return database result code */ enum GNUNET_DB_QueryStatus TMH_PG_lookup_mfa_challenge ( void *cls, const char *instance_id, - enum TALER_MERCHANT_MFA_CriticalOperation op, - const json_t *body, - struct TALER_MERCHANT_MFA_BodyHash *h_body, + uint64_t challenge_id, + const struct TALER_MERCHANT_MFA_BodyHash *h_body, struct TALER_MERCHANT_MFA_BodySalt *salt, + char **required_address, + enum TALER_MERCHANT_MFA_CriticalOperation *op, struct GNUNET_TIME_Absolute *confirmation_date, struct GNUNET_TIME_Absolute *retransmission_date, uint32_t *retry_counter, - enum TALER_MERCHANT_MFA_Channel *tan_channel, - uint64_t *challenge_id); + enum TALER_MERCHANT_MFA_Channel *tan_channel); #endif diff --git a/src/backenddb/pg_solve_mfa_challenge.c b/src/backenddb/pg_solve_mfa_challenge.c @@ -31,19 +31,13 @@ TMH_PG_solve_mfa_challenge ( void *cls, uint64_t challenge_id, const char *instance_id, - enum TALER_MERCHANT_MFA_CriticalOperation op, const struct TALER_MERCHANT_MFA_BodyHash *h_body, const char *solution, bool *solved, - uint32_t *retry_counter, - struct GNUNET_TIME_Absolute *retransmission_date, - enum TALER_MERCHANT_MFA_Channel *tan_channel) + uint32_t *retry_counter) { struct PostgresClosure *pg = cls; - bool no_date; bool no_match; - char *channel_str; - struct GNUNET_TIME_Absolute confirmation_date; struct GNUNET_PQ_QueryParam params[] = { GNUNET_PQ_query_param_uint64 (&challenge_id), GNUNET_PQ_query_param_string (instance_id), @@ -55,21 +49,9 @@ TMH_PG_solve_mfa_challenge ( GNUNET_PQ_result_spec_bool ("out_solved", solved), GNUNET_PQ_result_spec_allow_null ( - GNUNET_PQ_result_spec_absolute_time ("out_confirmation_date", - &confirmation_date), - &no_date), - GNUNET_PQ_result_spec_allow_null ( - GNUNET_PQ_result_spec_absolute_time ("out_retransmission_date", - retransmission_date), - &no_match), - GNUNET_PQ_result_spec_allow_null ( GNUNET_PQ_result_spec_uint32 ("out_retry_counter", retry_counter), &no_match), - GNUNET_PQ_result_spec_allow_null ( - GNUNET_PQ_result_spec_string ("out_tan_channel", - &channel_str), - &no_match), GNUNET_PQ_result_spec_end }; enum GNUNET_DB_QueryStatus qs; @@ -82,10 +64,7 @@ TMH_PG_solve_mfa_challenge ( "solve_mfa_challenge", "SELECT" " out_solved" - " ,out_confirmation_date" - " ,out_retransmission_date" " ,out_retry_counter" - " ,out_tan_channel" " FROM merchant_do_insert_issued_token" " ($1, $2, $3, $4);"); qs = GNUNET_PQ_eval_prepared_singleton_select (pg->conn, @@ -99,11 +78,6 @@ TMH_PG_solve_mfa_challenge ( GNUNET_PQ_cleanup_result (rs); return GNUNET_DB_STATUS_SUCCESS_NO_RESULTS; } - GNUNET_break (NULL != channel_str); - GNUNET_break ( (! solved) || - ( (! no_date) && - GNUNET_TIME_absolute_is_past (confirmation_date) ) ); - *tan_channel = TALER_MERCHANT_MFA_channel_from_string (channel_str); GNUNET_PQ_cleanup_result (rs); return qs; } diff --git a/src/backenddb/pg_solve_mfa_challenge.h b/src/backenddb/pg_solve_mfa_challenge.h @@ -34,16 +34,12 @@ * @param cls closure * @param challenge_id challenge ID to be solved * @param instance_id instance for which the challenge is being solved - * @param op operation which the challenge authorizes * @param h_body body of the operation the challenge authorizes * @param solution proposed solution to be checked against the actual code * @param[out] solved set to true if the challenge was solved by * @a solution * @param[out] retry_counter set to the number of attempts that remain * for solving the challenge (after this time) - * @param[out] retransmission_date set to when a fresh challenge - * may be transmitted - * @param[out] tan_channel set to the channel the challenge was sent on * @return database result code */ enum GNUNET_DB_QueryStatus @@ -51,12 +47,9 @@ TMH_PG_solve_mfa_challenge ( void *cls, uint64_t challenge_id, const char *instance_id, - enum TALER_MERCHANT_MFA_CriticalOperation op, const struct TALER_MERCHANT_MFA_BodyHash *h_body, const char *solution, bool *solved, - uint32_t *retry_counter, - struct GNUNET_TIME_Absolute *retransmission_date, - enum TALER_MERCHANT_MFA_Channel *tan_channel); + uint32_t *retry_counter); #endif diff --git a/src/backenddb/pg_solve_mfa_challenge.sql b/src/backenddb/pg_solve_mfa_challenge.sql @@ -14,21 +14,20 @@ -- TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> -- -CREATE OR REPLACE FUNCTION merchant_do_solve_mfa_challenge ( +DROP FUNCTION IF EXISTS merchant_do_solve_mfa_challenge; +CREATE FUNCTION merchant_do_solve_mfa_challenge ( IN in_challenge_id INT8, IN in_instance_id TEXT, IN in_h_body BYTEA, IN in_solution TEXT, OUT out_solved BOOLEAN, - OUT out_confirmation_date INT8, - OUT out_retransmission_date INT8, - OUT out_retry_counter INT4, - OUT out_tan_channel TEXT -- tan_enum + OUT out_retry_counter INT4 ) LANGUAGE plpgsql AS $$ DECLARE my_instance_id INT8; + my_confirmation_date INT8; DECLARE my_rec RECORD; BEGIN @@ -40,9 +39,7 @@ BEGIN -- Check if challenge exists and matches SELECT tc.confirmation_date - ,tc.retransmission_date ,tc.retry_counter - ,tc.tan_channel ,(tc.code = in_solution) AS solved INTO my_rec @@ -50,7 +47,10 @@ BEGIN WHERE tc.challenge_id = in_challenge_id AND tc.merchant_serial = my_instance_id AND tc.h_body = in_h_body - AND tc.expiration_date > EXTRACT(EPOCH FROM NOW()) * 1000000; + AND tc.expiration_date > + EXTRACT(EPOCH FROM NOW() + AT TIME ZONE 'UTC')::bigint + * 1000 * 1000; IF NOT FOUND THEN @@ -58,27 +58,31 @@ BEGIN RETURN; END IF; - out_confirmation_date = my_rec.confirmation_date; - out_retransmission_date = my_rec.retransmission_date; + my_confirmation_date = my_rec.confirmation_date; out_retry_counter = my_rec.retry_counter; - out_tan_channel = my_rec.tan_channel; out_solved = my_rec.solved; -- Check if already solved before - IF out_confirmation_date IS NOT NULL + IF my_confirmation_date IS NOT NULL THEN out_solved := TRUE; RETURN; END IF; + IF (0 == out_retry_counter) + THEN + out_solved = FALSE; + RETURN; + END IF; + IF out_solved THEN -- Newly solved, update DB! - out_confirmation_date = EXTRACT(EPOCH FROM NOW() - AT TIME ZONE 'UTC')::bigint - * 1000 * 1000; + my_confirmation_date = EXTRACT(EPOCH FROM NOW() + AT TIME ZONE 'UTC')::bigint + * 1000 * 1000; UPDATE tan_challenges - SET confirmation_date = out_confirmation_date + SET confirmation_date = my_confirmation_date WHERE challenge_id = in_challenge_id; ELSE -- Failed to solve, decrement retry counter diff --git a/src/include/taler_merchantdb_plugin.h b/src/include/taler_merchantdb_plugin.h @@ -4173,10 +4173,11 @@ struct TALER_MERCHANTDB_Plugin * * @param cls closure * @param instance_id instance for which the challenge is being looked up - * @param op operation that triggered the MFA request - * @param body request body to match against - * @param[out] h_body hash of the request body + * @param challenge_id set to the ID of the challenge + * @param h_body hash of the request body * @param[out] salt salt used to compute @a h_body + * @param[out] required address set to where the challenge is to be send + * @param[out] op operation that triggered the MFA request * @param[out] confirmation_date when was the challenge solved, * set to "GNUNET_TIME_ABSOLUTE_NEVER" if unsolved * @param[out] retransmission_date set to when a fresh challenge @@ -4184,22 +4185,21 @@ struct TALER_MERCHANTDB_Plugin * @param[out] retry_counter set to the number of attempts that remain * for solving the challenge (after this time) * @param[out] tan_channel which channel was used - * @param[out] challenge_id set to the ID of the new challenge * @return database result code */ enum GNUNET_DB_QueryStatus (*lookup_mfa_challenge)( void *cls, const char *instance_id, - enum TALER_MERCHANT_MFA_CriticalOperation op, - const json_t *body, - struct TALER_MERCHANT_MFA_BodyHash *h_body, + uint64_t challenge_id, + const struct TALER_MERCHANT_MFA_BodyHash *h_body, struct TALER_MERCHANT_MFA_BodySalt *salt, + char **required_address, + enum TALER_MERCHANT_MFA_CriticalOperation *op, struct GNUNET_TIME_Absolute *confirmation_date, struct GNUNET_TIME_Absolute *retransmission_date, uint32_t *retry_counter, - enum TALER_MERCHANT_MFA_Channel *tan_channel, - uint64_t *challenge_id); + enum TALER_MERCHANT_MFA_Channel *tan_channel); /** @@ -4211,16 +4211,12 @@ struct TALER_MERCHANTDB_Plugin * @param cls closure * @param challenge_id challenge ID to be solved * @param instance_id instance for which the challenge is being solved - * @param op operation which the challenge authorizes * @param h_body body of the operation the challenge authorizes * @param solution proposed solution to be checked against the actual code * @param[out] solved set to true if the challenge was solved by * @a solution - * @param[out] retransmission_date set to when a fresh challenge - * may be transmitted * @param[out] retry_counter set to the number of attempts that remain * for solving the challenge (after this time) - * @param[out] tan_channel set to the channel the challenge was sent on * @return database result code */ enum GNUNET_DB_QueryStatus @@ -4228,13 +4224,10 @@ struct TALER_MERCHANTDB_Plugin void *cls, uint64_t challenge_id, const char *instance_id, - enum TALER_MERCHANT_MFA_CriticalOperation op, const struct TALER_MERCHANT_MFA_BodyHash *h_body, const char *solution, bool *solved, - uint32_t *retry_counter, - struct GNUNET_TIME_Absolute *retransmission_date, - enum TALER_MERCHANT_MFA_Channel *tan_channel); + uint32_t *retry_counter); /** @@ -4274,8 +4267,8 @@ struct TALER_MERCHANTDB_Plugin * @param retansmission_date when do we next allow retransmission * of the challenge * @param tan_channel which channel was used - * @param tan_info information message to give to the user - * (about where the challenge was sent), can be NULL + * @param required_address address + * where the challenge is to be sent * @param[out] challenge_id set to the ID of the new challenge * @return database result code */ @@ -4290,7 +4283,7 @@ struct TALER_MERCHANTDB_Plugin struct GNUNET_TIME_Absolute expiration_date, struct GNUNET_TIME_Absolute retransmission_date, enum TALER_MERCHANT_MFA_Channel tan_channel, - const char *tan_info, + const char *required_address, uint64_t *challenge_id);