merchant

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

commit 697f247a0b91fd61ba65bd39d4272b099f597877
parent 9d7d7a2570125bbd80da68fc92de850f9e98641c
Author: Christian Grothoff <christian@grothoff.org>
Date:   Mon,  1 Sep 2025 21:00:59 +0200

make MFA API async, complete it a bit more

Diffstat:
Msrc/backend/Makefile.am | 2++
Msrc/backend/taler-merchant-httpd.c | 2++
Msrc/backend/taler-merchant-httpd_mfa.c | 810+++++++++++++++++++++++++++++++++++++++++++++++++------------------------------
Msrc/backend/taler-merchant-httpd_mfa.h | 38+++++++++++++++++++++++++++++++++-----
Msrc/include/taler_merchant_util.h | 2+-
5 files changed, 542 insertions(+), 312 deletions(-)

diff --git a/src/backend/Makefile.am b/src/backend/Makefile.am @@ -80,6 +80,8 @@ taler_merchant_httpd_SOURCES = \ taler-merchant-httpd_helper.h \ taler-merchant-httpd_mhd.c \ taler-merchant-httpd_mhd.h \ + taler-merchant-httpd_mfa.c \ + taler-merchant-httpd_mfa.h \ taler-merchant-httpd_private-delete-account-ID.c \ taler-merchant-httpd_private-delete-account-ID.h \ taler-merchant-httpd_private-delete-categories-ID.c \ diff --git a/src/backend/taler-merchant-httpd.c b/src/backend/taler-merchant-httpd.c @@ -35,6 +35,7 @@ #include "taler-merchant-httpd_get-templates-ID.h" #include "taler-merchant-httpd_helper.h" #include "taler-merchant-httpd_mhd.h" +#include "taler-merchant-httpd_mfa.h" #include "taler-merchant-httpd_private-delete-account-ID.h" #include "taler-merchant-httpd_private-delete-categories-ID.h" #include "taler-merchant-httpd_private-delete-instances-ID.h" @@ -634,6 +635,7 @@ do_shutdown (void *cls) TMH_force_gorc_resume (); TMH_force_wallet_get_order_resume (); TMH_force_wallet_refund_order_resume (); + TMH_mfa_done (); TALER_MHD_daemons_destroy (); if (NULL != instance_eh) { diff --git a/src/backend/taler-merchant-httpd_mfa.c b/src/backend/taler-merchant-httpd_mfa.c @@ -40,392 +40,590 @@ /** - * Setup challenge @a code for @a channel and send it to the - * @a required_address; on success, set @a expiration_date - * and @a retransmission_date based on the @a channel. Return - * a @a hint for the user. + * Internal state for MFA processing. + */ +struct MfaState +{ + + /** + * Kept in a DLL. + */ + struct MfaState *next; + + /** + * 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 channel how to send the challenge - * @param required_address where to send the challenge - * @param[out] code set to challenge code - * @param[out] expiration_date when the challenge expires - * @param[out] retransmission_date when we may send another challenge - * to the same @a required_address - * @param[out] hint set to a hint to return to the user - * about where he will receive the challenge - * @return #GNUNET_OK on success + * @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 enum GNUNET_DB_QueryStatus -setup_challenge (enum TALER_MERCHANT_MFA_Channel channel, - const char *required_address, - char **code, - struct GNUNET_TIME_Absolute *expiration_date, - struct GNUNET_TIME_Absolute *retransmission_date, - char **hint) +static void +finish_mfa (struct MfaState *mfa, + enum GNUNET_GenericReturnValue ret, + unsigned int http_status, + struct MHD_Response *response) { - const char *prog; + GNUNET_CONTAINER_DLL_remove (mfa_head, + mfa_tail, + mfa); + mfa->cb (mfa->cb_cls, + ret, + http_status, + response); + if (NULL != mfa->cwh) + { + GNUNET_wait_child_cancel (mfa->cwh); + mfa->cwh = NULL; + } + if (NULL != mfa->child) + { + (void) GNUNET_OS_process_kill (mfa->child, + SIGKILL); + GNUNET_break (GNUNET_OK == + GNUNET_OS_process_wait (mfa->child)); + mfa->child = NULL; + } + 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)) + { + finish_mfa (mfa, + GNUNET_SYSERR, + 0, + NULL); + } +} + + +/** + * Obtain hint about the @a target_address of type @a channel to + * return to the client. + * + * @param channel type of challenge + * @param target_address address we will sent the challenge to + * @return hint for the user about the address + */ +static char * +get_hint (enum TALER_MERCHANT_MFA_Channel channel, + const char *target_address) +{ switch (channel) { case TALER_MERCHANT_MFA_CHANNEL_NONE: GNUNET_assert (0); - break; + return NULL; case TALER_MERCHANT_MFA_CHANNEL_SMS: - *expiration_date - = GNUNET_TIME_relative_to_absolute (GNUNET_TIME_UNIT_HOURS); - *retransmission_date - = GNUNET_TIME_relative_to_absolute (GNUNET_TIME_UNIT_HOURS); - GNUNET_asprintf (code, - "%llu", - (unsigned long long) - GNUNET_CRYPTO_random_u64 (GNUNET_CRYPTO_QUALITY_NONCE, - 100000000)); - prog = "taler-merchant-helper-send-sms"; - *hint = NULL; // FIXME: generate nice hint! - break; + { + 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: - *expiration_date - = GNUNET_TIME_relative_to_absolute (GNUNET_TIME_UNIT_HOURS); - *retransmission_date - = GNUNET_TIME_relative_to_absolute (GNUNET_TIME_UNIT_HOURS); - GNUNET_asprintf (code, - "%llu", - (unsigned long long) - GNUNET_CRYPTO_random_u64 (GNUNET_CRYPTO_QUALITY_NONCE, - 100000000)); - prog = "taler-merchant-helper-send-email"; - *hint = NULL; // FIXME: generate nice hint! - break; + { + 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: - *expiration_date - = GNUNET_TIME_relative_to_absolute (OTP_TIMEOUT); - *retransmission_date - = GNUNET_TIME_relative_to_absolute (OTP_TIMEOUT); - GNUNET_asprintf (hint, - "You have %s to submit the correct OTP code", - GNUNET_TIME_relative_to_string (OTP_TIMEOUT, - false)); - code = GNUNET_strdup ("not implemented"); - return GNUNET_OK; + GNUNET_break (0); + return GNUNET_strdup ("TOTP is not implemented: #10327"); } + GNUNET_break (0); + return NULL; +} + - // run 'prog' with 'code'! - GNUNET_break (0); // not implemented - return GNUNET_SYSERR; +/** + * 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 the @a retransmit_date is in the past, send a fresh challenge. + * Generate a response to the client pointing them to the challenge + * that was sent. * - * @param[in,out] hc handler context of the connection to authorize - * @param op operation for which we are performing - * @param challenge_id challenge to update - * @param channel how to send the challenge - * @param required_address where to send the challenge - * @param retransmit_date when to send the challenge at the earliest - * @param[out] hint hint to return to the user - * @return #GNUNET_OK on success (including if @a retransmit_date is in the - * future) - * #GNUNET_NO if we failed to send a challenge and queued an - * error response - * #GNUNET_SYSERR if the client connection must simply be closed + * @param[in] mfa process to generate response for */ -static enum GNUNET_DB_QueryStatus -retransmit_challenge ( - struct TMH_HandlerContext *hc, - uint64_t challenge_id, - enum TALER_MERCHANT_MFA_Channel channel, - const char *required_address, - struct GNUNET_TIME_Absolute retransmit_date, - char **hint) +static void +respond_with_challenge (struct MfaState *mfa) { - enum GNUNET_DB_QueryStatus res; - char *code; + 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. + * + * @param cls our `struct ANASTASIS_AUHTORIZATION_State` + * @param type type of the process + * @param exit_code status code of the process + */ +static void +transmission_done_cb (void *cls, + enum GNUNET_OS_ProcessStatusType type, + long unsigned int exit_code) +{ + struct MfaState *mfa = cls; + enum GNUNET_DB_QueryStatus qs; - *hint = NULL; - if (GNUNET_TIME_absolute_is_future (retransmit_date)) + mfa->cwh = NULL; + if (NULL != mfa->child) { - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Challenge already sent to `%s' recently, not sending again\n", - required_address); - GNUNET_asprintf ( - hint, - "Challenge will not be retransmitted before %s", - GNUNET_STRINGS_absolute_time_to_string (retransmit_date)); - return GNUNET_OK; + GNUNET_OS_process_destroy (mfa->child); + mfa->child = NULL; } - res = setup_challenge (channel, - required_address, - &code, - &expiration_date, - &retransmit_date, - hint); - if (GNUNET_OK != res) + if (0 == mfa->challenge_id) { - GNUNET_break (0); - return res; + /* 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); } - - qs = TMH_db->update_mfa_challenge (TMH_db->cls, - challenge_id, - code, - MAX_SOLUTIONS, - expiration_date, - retransmit_date); - GNUNET_free (code); 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); - ret = TALER_MHD_reply_with_error ( - hc->connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - (GNUNET_DB_STATUS_SOFT_ERROR == qs) - ? TALER_EC_GENERIC_DB_SOFT_FAILURE - : TALER_EC_GENERIC_DB_COMMIT_FAILED, - NULL); - return (MHD_NO == ret) ? GNUNET_SYSERR : GNUNET_NO; + 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_break (0); - ret = TALER_MHD_reply_with_error ( - hc->connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_INVARIANT_FAILURE, - "no results on INSERT, but success?"); - return (MHD_NO == ret) ? GNUNET_SYSERR : GNUNET_NO; + respond_with_error (mfa, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_INVARIANT_FAILURE, + "no results on INSERT, but success?"); + return; case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: break; } - return GNUNET_OK; - + respond_with_challenge (mfa); } /** - * Create a challenge in the backend and transmit it to - * the @a required_address. + * Setup challenge code for @a mfa and send it to the + * @a required_address; on success. * - * @param[in,out] hc handler context of the connection to authorize - * @param op operation for which we are performing - * @param h_body salted hash of the request body - * @param salt salt used for @a h_body - * @param channel how to send the challenge + * @param[in,out] mfa process to send the challenge for * @param required_address where to send the challenge - * @param[out] challenge_id set to the numeric ID of the - * challenge - * @param[out] hint set to a hint to return to the user - * about where he will receive the challenge */ -static enum GNUNET_DB_QueryStatus -create_challenge ( - struct TMH_HandlerContext *hc, - enum TALER_MERCHANT_MFA_CriticalOperation op, - const struct TALER_MERCHANT_MFA_BodyHash *h_body, - const struct TALER_MERCHANT_MFA_BodySalt *salt, - enum TALER_MERCHANT_MFA_Channel channel, - const char *required_address, - uint64_t *challenge_id, - char **hint) +static void +send_challenge (struct MfaState *mfa, + const char *required_address) { - enum GNUNET_DB_QueryStatus qs; - char *code; - struct GNUNET_TIME_Absolute expiration_date; - struct GNUNET_TIME_Absolute retransmission_date; - enum GNUNET_DB_QueryStatus res; - - *hint = NULL; - res = setup_challenge (channel, - required_address, - &code, - &expiration_date, - &retransmission_date, - hint); - if (GNUNET_OK != res) + const char *prog; + + switch (mfa->next_challenge) { - GNUNET_break (0); - return res; + 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; } - qs = TMH_db->create_mfa_challenge (TMH_db->cls, - hc->instance->settings.id, - op, - h_body, - salt, - code, - expiration_date, - retransmission_date, - channel, - *hint, - challenge_id); - GNUNET_free (code); - switch (qs) { - case GNUNET_DB_STATUS_HARD_ERROR: - case GNUNET_DB_STATUS_SOFT_ERROR: - GNUNET_break (0); - ret = TALER_MHD_reply_with_error ( - hc->connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - (GNUNET_DB_STATUS_SOFT_ERROR == qs) - ? TALER_EC_GENERIC_DB_SOFT_FAILURE - : TALER_EC_GENERIC_DB_COMMIT_FAILED, - NULL); - return (MHD_NO == ret) ? GNUNET_SYSERR : GNUNET_NO; - case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: - GNUNET_break (0); - ret = TALER_MHD_reply_with_error ( - hc->connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_INVARIANT_FAILURE, - "no results on INSERT, but success?"); - return (MHD_NO == ret) ? GNUNET_SYSERR : GNUNET_NO; - case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: - break; + /* Start child process and feed pipe */ + struct GNUNET_DISK_PipeHandle *p; + struct GNUNET_DISK_FileHandle *pipe_stdin; + + 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) + { + GNUNET_DISK_pipe_close (p); + respond_with_error (mfa, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_ANASTASIS_SMS_HELPER_EXEC_FAILED, + "exec"); + return; + } + + 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 *off = mfa->msg; + size_t left = strlen (off); + + while (0 != left) + { + ssize_t ret; + + ret = GNUNET_DISK_file_write (pipe_stdin, + off, + left); + if (ret <= 0) + { + respond_with_error (mfa, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_ANASTASIS_SMS_HELPER_EXEC_FAILED, + "write"); + return; + } + mfa->msg_off += ret; + off += ret; + left -= ret; + } + GNUNET_DISK_file_close (pipe_stdin); + } } - return GNUNET_OK; + mfa->cwh = GNUNET_wait_child (mfa->child, + &transmission_done_cb, + mfa); } -enum GNUNET_DB_QueryStatus +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[]) + const char *required_addresses[], + TMH_MFA_ResultCallback cb, + void *cb_cls) { - enum TALER_MERCHANT_MFA_Channel last_ch; + struct MfaState *mfa; + enum TALER_MERCHANT_MFA_Channel last_channel; + uint32_t retry_counter; enum GNUNET_DB_QueryStatus qs; - struct TALER_MERCHANT_MFA_BodyHash h_body; - struct TALER_MERCHANT_MFA_BodySalt salt; struct GNUNET_TIME_Absolute confirmation_date; - struct GNUNET_TIME_Absolute retransmission_date; - uint32_t retry_counter; - uint64_t challenge_id; - MHD_RESULT ret; - enum GNUNET_DB_QueryStatus res; - char *hint = NULL; - if (TALER_MERCHANT_MFA_CHANNEL_NONE == required_trans[0]) - return GNUNET_OK; + 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]) + { + finish_mfa (mfa, + GNUNET_OK, + 0, + NULL); + return; + } qs = TMH_db->lookup_mfa_challenge (TMH_db->cls, hc->instance->settings.id, op, hc->request_body, - &h_body, - &salt, - &conf_date, - &retransmit_date, + &mfa->h_body, + &mfa->salt, + &confirmation_date, + &mfa->retransmission_date, &retry_counter, &last_channel, - &challenge_id); + &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); - ret = TALER_MHD_reply_with_error ( - hc->connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - (GNUNET_DB_STATUS_SOFT_ERROR == qs) - ? TALER_EC_GENERIC_DB_SOFT_FAILURE - : TALER_EC_GENERIC_DB_COMMIT_FAILED, - NULL); - return (MHD_NO == ret) ? GNUNET_SYSERR : GNUNET_NO; + 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, - &salt, - sizeof (salt)); + &mfa->salt, + sizeof (mfa->salt)); TALER_MERCHANT_mfa_body_hash (hc->request_body, - &salt, - &h_body); - next_challenge = required_trans[0]; - retransmission_date = GNUNET_TIME_UNIT_ZERO_ABS; /* now */ - retry_counter = MAX_SOLUTIONS; - res = create_challenge (hc, - op, - &h_body, - &salt, - next_challenge, - required_addresses[0], - &challenge_id, - &hint); - if (GNUNET_OK != res) - return res; - break; + &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; + } + for (unsigned int i = 0; + TALER_MERCHANT_MFA_CHANNEL_NONE != required_tans[i]; + i++) + { + if (last_channel == required_tans[i]) { - bool found = false; - - for (unsigned int i=0; - TALER_MERCHANT_MFA_CHANNEL_NONE != required_trans[i]; - i++) + if (GNUNET_TIME_absolute_is_past (confirmation_date)) { - if (last_channel == required_trans[i]) + /* Last challenge WAS solved, move on! */ + mfa->next_challenge = required_tans[i + 1]; + if (TALER_MERCHANT_MFA_CHANNEL_NONE == mfa->next_challenge) { - if (GNUNET_TIME_absolute_is_past (confirmation_date)) - { - /* Last challenge WAS solved, move on! */ - next_challenge = required_trans[i + 1]; - retransmission_date = GNUNET_TIME_UNIT_ZERO_ABS; /* now */ - retry_counter = MAX_SOLUTIONS; - if (TALER_MERCHANT_MFA_CHANNEL_NONE == next_challenge) - return GNUNET_OK; - res = create_challenge (hc, - op, - &h_body, - &salt, - next_challenge, - required_addresses[i + 1], - &challenge_id, - &hint); - if (GNUNET_OK != res) - return res; - } - else - { - /* challenge was not yet solved, possibly re-transmit - a new one */ - next_challenge = last_channel; - res = retransmit_challenge (hc, - challenge_id, - last_channel, - required_addresses[i], - retransmit_date, - &hint); - if (GNUNET_OK != res) - return res; - } - found = true; - break; + 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; } - if (! found) + else { - /* challenge was satisfied that is not in our list! */ - GNUNET_break (0); - ret = TALER_MHD_reply_with_error ( - hc->connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_INVARIANT_FAILURE, - "unexpected challenge already satisfied"); - return (MHD_NO == ret) ? GNUNET_SYSERR : GNUNET_NO; + /* 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; } } - break; } - - /* Return HTTP response that a challenge was sent */ - ret = TALER_MHD_REPLY_JSON_PACK ( - hc->connection, - MHD_HTTP_FORBIDDEN, /* FIXME: check HTTP status code! Maybe use ACCEPTED? */ - // FIXME: add ec!? - GNUNET_JSON_pack_string ("hint", - hint), - GNUNET_JSON_pack_uint64 ("challenge_id", - challenge_id), - GNUNET_JSON_pack_auto_from_type ("h_body", - &h_body)); - GNUNET_free (hint); - return (MHD_NO == ret) ? GNUNET_SYSERR : GNUNET_NO; + /* 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"); } diff --git a/src/backend/taler-merchant-httpd_mfa.h b/src/backend/taler-merchant-httpd_mfa.h @@ -30,6 +30,24 @@ /** + * Function called with the result of an MFA operation. + * + * @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 + */ +typedef void +(*TMH_MFA_ResultCallback)(void *cls, + enum GNUNET_GenericReturnValue res, + unsigned int http_status, + struct MHD_Response *response); + + +/** * 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 @@ -46,15 +64,25 @@ * @param required_addresses array of addresses to use for * the respective challenge, NULL-terminated, same length * as @a required_tans - * @return #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 cb continuation to call with the result + * @param cb_cls closure for @a cb */ -enum GNUNET_DB_QueryStatus +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[]); + 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); + #endif diff --git a/src/include/taler_merchant_util.h b/src/include/taler_merchant_util.h @@ -56,7 +56,7 @@ enum TALER_MERCHANT_MFA_Channel TALER_MERCHANT_MFA_CHANNEL_EMAIL, /** - * TOTP ("totp"). (Not yet implemented.) + * TOTP ("totp"). (Not yet implemented, #10327.) */ TALER_MERCHANT_MFA_CHANNEL_TOTP };