commit 1c71613d16918ab9d97e75f336a393d79892485e
parent ee187192164b1bd0a4f76f5416c5a2d9dd93567c
Author: Christian Grothoff <christian@grothoff.org>
Date: Tue, 2 Sep 2025 23:00:06 +0200
fix tutorial build, tutorial is no longer in prebuilt
Diffstat:
4 files changed, 921 insertions(+), 0 deletions(-)
diff --git a/src/backend/taler-merchant-httpd_post-challenge-ID-confirm.c b/src/backend/taler-merchant-httpd_post-challenge-ID-confirm.c
@@ -0,0 +1,126 @@
+/*
+ This file is part of TALER
+ (C) 2025 Taler Systems SA
+
+ TALER 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.
+
+ TALER 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 General Public License for more details.
+
+ You should have received a copy of the GNU General Public
+ License along with TALER; see the file COPYING. If not,
+ see <http://www.gnu.org/licenses/>
+*/
+
+/**
+ * @file taler-merchant-httpd_post-challenge-ID-confirm.c
+ * @brief endpoint to solve MFA challenge
+ * @author Christian Grothoff
+ */
+#include "platform.h"
+#include "taler-merchant-httpd.h"
+#include "taler-merchant-httpd_mfa.h"
+#include "taler-merchant-httpd_post-challenge-ID-confirm.h"
+
+
+MHD_RESULT
+TMH_post_challenge_ID_confirm (const struct TMH_RequestHandler *rh,
+ struct MHD_Connection *connection,
+ struct TMH_HandlerContext *hc)
+{
+ uint64_t challenge_serial;
+ struct TALER_MERCHANT_MFA_BodyHash h_body;
+ const char *tan;
+ struct GNUNET_JSON_Specification spec[] = {
+ GNUNET_JSON_spec_string ("tan",
+ &tan),
+ GNUNET_JSON_spec_end ()
+ };
+ enum GNUNET_DB_QueryStatus qs;
+ bool solved;
+ uint32_t retry_counter;
+ enum GNUNET_GenericReturnValue ret;
+
+ ret = TMH_mfa_parse_challenge_id (hc,
+ hc->infix,
+ &challenge_serial,
+ &h_body);
+ if (GNUNET_OK != ret)
+ return (GNUNET_SYSERR == ret) ? MHD_NO : MHD_YES;
+ {
+ enum GNUNET_GenericReturnValue res;
+
+ res = TALER_MHD_parse_json_data (hc->connection,
+ hc->request_body,
+ spec);
+ if (GNUNET_OK != res)
+ {
+ GNUNET_break_op (0);
+ return (GNUNET_NO == res)
+ ? MHD_YES
+ : MHD_NO;
+ }
+ }
+ qs = TMH_db->solve_mfa_challenge (TMH_db->cls,
+ challenge_serial,
+ hc->instance->settings.id,
+ &h_body,
+ tan,
+ &solved,
+ &retry_counter);
+ switch (qs)
+ {
+ case GNUNET_DB_STATUS_HARD_ERROR:
+ GNUNET_break (0);
+ return TALER_MHD_reply_with_error (
+ hc->connection,
+
+ MHD_HTTP_INTERNAL_SERVER_ERROR,
+ TALER_EC_GENERIC_DB_COMMIT_FAILED,
+ "solve_mfa_challenge");
+ case GNUNET_DB_STATUS_SOFT_ERROR:
+ GNUNET_break (0);
+ return TALER_MHD_reply_with_error (
+ hc->connection,
+ MHD_HTTP_INTERNAL_SERVER_ERROR,
+ TALER_EC_GENERIC_DB_SOFT_FAILURE,
+ "solve_mfa_challenge");
+ case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS:
+ GNUNET_break_op (0);
+ return TALER_MHD_reply_with_error (
+ hc->connection,
+ MHD_HTTP_NOT_FOUND,
+ TALER_EC_MERCHANT_TAN_CHALLENGE_UNKNOWN,
+ hc->infix);
+ case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT:
+ break;
+ }
+ if (0 == retry_counter)
+ {
+ return TALER_MHD_reply_with_error (
+ hc->connection,
+ MHD_HTTP_TOO_MANY_REQUESTS,
+ TALER_EC_MERCHANT_TAN_TOO_MANY_ATTEMPTS,
+ NULL);
+ }
+ if (! solved)
+ {
+ return TALER_MHD_REPLY_JSON_PACK (
+ hc->connection,
+ MHD_HTTP_CONFLICT,
+ TALER_MHD_PACK_EC (TALER_EC_MERCHANT_TAN_CHALLENGE_FAILED),
+ GNUNET_JSON_pack_uint64 ("retry_counter",
+ retry_counter));
+ }
+ return TALER_MHD_reply_static (
+ hc->connection,
+ MHD_HTTP_NO_CONTENT,
+ NULL,
+ NULL,
+ 0);
+}
diff --git a/src/backend/taler-merchant-httpd_post-challenge-ID-confirm.h b/src/backend/taler-merchant-httpd_post-challenge-ID-confirm.h
@@ -0,0 +1,45 @@
+/*
+ This file is part of TALER
+ (C) 2025 Taler Systems SA
+
+ TALER 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.
+
+ TALER 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 General Public License for more details.
+
+ You should have received a copy of the GNU General Public
+ License along with TALER; see the file COPYING. If not,
+ see <http://www.gnu.org/licenses/>
+*/
+/**
+ * @file taler-merchant-httpd_post-challenge-ID-confirm.h
+ * @brief endpoint to solve MFA challenge
+ * @author Christian Grothoff
+ */
+#ifndef TALER_EXCHANGE_HTTPD_POST_CHALLENGE_ID_CONFIRM_H
+#define TALER_EXCHANGE_HTTPD_POST_CHALLENGE_ID_CONFIRM_H
+
+#include <microhttpd.h>
+#include "taler-merchant-httpd.h"
+
+
+/**
+ * Client submits TAN code to solve multi-factor authentication challenge.
+ *
+ * @param rh context of the handler
+ * @param connection the MHD connection to handle
+ * @param[in,out] hc context with further information about the request
+ * @return MHD result code
+ */
+MHD_RESULT
+TMH_post_challenge_ID_confirm (const struct TMH_RequestHandler *rh,
+ struct MHD_Connection *connection,
+ struct TMH_HandlerContext *hc);
+
+
+#endif
diff --git a/src/backend/taler-merchant-httpd_post-challenge-ID.c b/src/backend/taler-merchant-httpd_post-challenge-ID.c
@@ -0,0 +1,701 @@
+/*
+ This file is part of TALER
+ (C) 2025 Taler Systems SA
+
+ TALER 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.
+
+ TALER 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 General Public License for more details.
+
+ You should have received a copy of the GNU General Public
+ License along with TALER; see the file COPYING. If not,
+ see <http://www.gnu.org/licenses/>
+*/
+
+/**
+ * @file taler-merchant-httpd_post-challenge-ID.c
+ * @brief endpoint to trigger sending MFA challenge
+ * @author Christian Grothoff
+ */
+#include "platform.h"
+#include "taler-merchant-httpd.h"
+#include "taler-merchant-httpd_mfa.h"
+#include "taler-merchant-httpd_post-challenge-ID.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?
+ */
+#define OTP_TIMEOUT GNUNET_TIME_relative_multiply (GNUNET_TIME_UNIT_SECONDS, 30)
+
+
+/**
+ * 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;
+
+ /**
+ * Address where to send the challenge.
+ */
+ char *required_address;
+
+ /**
+ * Message to send.
+ */
+ char *msg;
+
+ /**
+ * Offset of transmission in msg.
+ */
+ size_t msg_off;
+
+ /**
+ * ID of our challenge.
+ */
+ uint64_t challenge_id;
+
+ /**
+ * Salted hash over the request body.
+ */
+ struct TALER_MERCHANT_MFA_BodyHash h_body;
+
+ /**
+ * Channel to use for the challenge.
+ */
+ enum TALER_MERCHANT_MFA_Channel channel;
+
+ enum
+ {
+ MFA_PHASE_PARSE = 0,
+ MFA_PHASE_LOOKUP,
+ MFA_PHASE_SENDING,
+ MFA_PHASE_SUSPENDING,
+ MFA_PHASE_SENT,
+ MFA_PHASE_RETURN_YES,
+ MFA_PHASE_RETURN_NO,
+
+ } phase;
+
+
+ /**
+ * #GNUNET_NO if the @e connection was not suspended,
+ * #GNUNET_YES if the @e connection was suspended,
+ * #GNUNET_SYSERR if @e connection was resumed to as
+ * part of #THM_mfa_done during shutdown.
+ */
+ enum GNUNET_GenericReturnValue suspended;
+
+ /**
+ * Set to true if sending worked.
+ */
+ bool send_ok;
+};
+
+
+/**
+ * Kept in a DLL.
+ */
+static struct MfaState *mfa_head;
+
+/**
+ * Kept in a DLL.
+ */
+static struct MfaState *mfa_tail;
+
+
+/**
+ * Clean up @a mfa process.
+ *
+ * @param[in] cls the `struct MfaState` to clean up
+ */
+static void
+mfa_context_cleanup (void *cls)
+{
+ struct MfaState *mfa = cls;
+
+ GNUNET_CONTAINER_DLL_remove (mfa_head,
+ mfa_tail,
+ mfa);
+ 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->required_address);
+ GNUNET_free (mfa->msg);
+ GNUNET_free (mfa->code);
+ GNUNET_free (mfa);
+}
+
+
+void
+TMH_challenge_done ()
+{
+ for (struct MfaState *mfa = mfa_head;
+ NULL != mfa;
+ mfa = mfa->next)
+ {
+ if (GNUNET_YES == mfa->suspended)
+ {
+ mfa->suspended = GNUNET_SYSERR;
+ MHD_resume_connection (mfa->hc->connection);
+ }
+ }
+}
+
+
+/**
+ * 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);
+ 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:
+ GNUNET_break (0);
+ return GNUNET_strdup ("TOTP is not implemented: #10327");
+ }
+ GNUNET_break (0);
+ return NULL;
+}
+
+
+/**
+ * Send the given @a response for the @a mfa request.
+ *
+ * @param[in,out] mfa process to generate an error response for
+ * @param response_code response code to use
+ * @param[in] response response data to send back
+ */
+static void
+respond_to_challenge_with_response (struct MfaState *mfa,
+ unsigned int response_code,
+ struct MHD_Response *response)
+{
+ MHD_RESULT res;
+
+ res = MHD_queue_response (mfa->hc->connection,
+ response_code,
+ response);
+ MHD_destroy_response (response);
+ mfa->phase = (MHD_NO == res)
+ ? MFA_PHASE_RETURN_NO
+ : MFA_PHASE_RETURN_YES;
+}
+
+
+/**
+ * Generate an error for @a mfa.
+ *
+ * @param[in,out] 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)
+{
+ respond_to_challenge_with_response (
+ mfa,
+ http_status,
+ TALER_MHD_make_error (ec,
+ hint));
+}
+
+
+/**
+ * Challenge code transmission complete. Continue based on the result.
+ *
+ * @param[in,out] mfa process to send the challenge for
+ */
+static void
+phase_sent (struct MfaState *mfa)
+{
+ enum GNUNET_DB_QueryStatus qs;
+ char *address_hint;
+
+ if (! mfa->send_ok)
+ {
+ respond_with_error (mfa,
+ MHD_HTTP_INTERNAL_SERVER_ERROR,
+ TALER_EC_MERCHANT_TAN_MFA_HELPER_EXEC_FAILED,
+ "process exited with error");
+ return;
+ }
+ qs = TMH_db->update_mfa_challenge (TMH_db->cls,
+ mfa->challenge_id,
+ mfa->code,
+ MAX_SOLUTIONS,
+ mfa->expiration_date,
+ mfa->retransmission_date);
+ 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,
+ "update_mfa_challenge");
+ 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,
+ "update_mfa_challenge");
+ return;
+ 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;
+ case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT:
+ break;
+ }
+ address_hint = get_hint (mfa->channel,
+ mfa->required_address);
+ respond_to_challenge_with_response (
+ mfa,
+ MHD_HTTP_ACCEPTED,
+ TALER_MHD_MAKE_JSON_PACK (
+ GNUNET_JSON_pack_string ("address_hint",
+ address_hint),
+ GNUNET_JSON_pack_string ("challenge_type",
+ TALER_MERCHANT_MFA_channel_to_string (
+ mfa->channel)),
+ GNUNET_JSON_pack_uint64 ("challenge_id",
+ mfa->challenge_id),
+ GNUNET_JSON_pack_data_auto ("h_body",
+ &mfa->h_body)));
+ GNUNET_free (address_hint);
+}
+
+
+/**
+ * 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;
+
+ mfa->cwh = NULL;
+ if (NULL != mfa->child)
+ {
+ GNUNET_OS_process_destroy (mfa->child);
+ mfa->child = NULL;
+ }
+ mfa->send_ok = ( (GNUNET_OS_PROCESS_EXITED == type) &&
+ (0 == exit_code) );
+ if (! mfa->send_ok)
+ GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
+ "MFA helper failed with status %d/%u\n",
+ (int) type,
+ (unsigned int) exit_code);
+ mfa->phase = MFA_PHASE_SENT;
+ GNUNET_assert (GNUNET_YES == mfa->suspended);
+ mfa->suspended = GNUNET_NO;
+ MHD_resume_connection (mfa->hc->connection);
+ TALER_MHD_daemon_trigger ();
+}
+
+
+/**
+ * Setup challenge code for @a mfa and send it to the
+ * @a required_address; on success.
+ *
+ * @param[in,out] mfa process to send the challenge for
+ * @param required_address where to send the challenge
+ */
+static void
+phase_send_challenge (struct MfaState *mfa)
+{
+ const char *prog;
+
+ switch (mfa->channel)
+ {
+ 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 = TMH_helper_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 = TMH_helper_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_FEATURE_NOT_IMPLEMENTED,
+ "#10327");
+ return;
+ }
+ if (NULL == prog)
+ {
+ respond_with_error (
+ mfa,
+ MHD_HTTP_INTERNAL_SERVER_ERROR,
+ TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE,
+ TALER_MERCHANT_MFA_channel_to_string (mfa->channel));
+ return;
+ }
+ {
+ /* 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_ALLOCATION_FAILURE,
+ "pipe");
+ return;
+ }
+ mfa->child = GNUNET_OS_start_process (GNUNET_OS_INHERIT_STD_ERR,
+ p,
+ NULL,
+ NULL,
+ prog,
+ prog,
+ mfa->required_address,
+ NULL);
+ if (NULL == mfa->child)
+ {
+ GNUNET_DISK_pipe_close (p);
+ respond_with_error (mfa,
+ MHD_HTTP_INTERNAL_SERVER_ERROR,
+ TALER_EC_MERCHANT_TAN_MFA_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_MERCHANT_TAN_MFA_HELPER_EXEC_FAILED,
+ "write");
+ return;
+ }
+ mfa->msg_off += ret;
+ off += ret;
+ left -= ret;
+ }
+ GNUNET_DISK_file_close (pipe_stdin);
+ }
+ }
+ mfa->phase = MFA_PHASE_SUSPENDING;
+}
+
+
+/**
+ * Lookup challenge in DB.
+ *
+ * @param[in,out] mfa process to parse data for
+ */
+static void
+phase_lookup (struct MfaState *mfa)
+{
+ enum GNUNET_DB_QueryStatus qs;
+ uint32_t retry_counter;
+ struct GNUNET_TIME_Absolute confirmation_date;
+ struct GNUNET_TIME_Absolute retransmission_date;
+ struct TALER_MERCHANT_MFA_BodySalt salt;
+ enum TALER_MERCHANT_MFA_CriticalOperation op;
+
+ qs = TMH_db->lookup_mfa_challenge (TMH_db->cls,
+ mfa->hc->instance->settings.id,
+ mfa->challenge_id,
+ &mfa->h_body,
+ &salt,
+ &mfa->required_address,
+ &op,
+ &confirmation_date,
+ &retransmission_date,
+ &retry_counter,
+ &mfa->channel);
+ 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,
+ "lookup_mfa_challenge");
+ 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,
+ "lookup_mfa_challenge");
+ return;
+ case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS:
+ GNUNET_break (0);
+ respond_with_error (mfa,
+ MHD_HTTP_NOT_FOUND,
+ TALER_EC_MERCHANT_TAN_CHALLENGE_UNKNOWN,
+ mfa->hc->infix);
+ return;
+ case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT:
+ break;
+ }
+ if (! GNUNET_TIME_absolute_is_future (confirmation_date))
+ {
+ /* was already solved */
+ respond_with_error (mfa,
+ MHD_HTTP_GONE,
+ TALER_EC_MERCHANT_TAN_CHALLENGE_SOLVED,
+ NULL);
+ return;
+ }
+ if (! GNUNET_TIME_absolute_is_future (retransmission_date))
+ {
+ /* too early to try again */
+ respond_with_error (mfa,
+ MHD_HTTP_TOO_MANY_REQUESTS,
+ TALER_EC_MERCHANT_TAN_TOO_EARLY,
+ GNUNET_TIME_absolute2s (retransmission_date));
+ return;
+ }
+ mfa->phase++;
+}
+
+
+/**
+ * Parse challenge request.
+ *
+ * @param[in,out] mfa process to parse data for
+ */
+static void
+phase_parse (struct MfaState *mfa)
+{
+ struct TMH_HandlerContext *hc = mfa->hc;
+ enum GNUNET_GenericReturnValue ret;
+
+ ret = TMH_mfa_parse_challenge_id (hc,
+ hc->infix,
+ &mfa->challenge_id,
+ &mfa->h_body);
+ if (GNUNET_OK != ret)
+ {
+ mfa->phase = (GNUNET_NO == ret)
+ ? MFA_PHASE_RETURN_YES
+ : MFA_PHASE_RETURN_NO;
+ return;
+ }
+ mfa->phase++;
+}
+
+
+MHD_RESULT
+TMH_post_challenge_ID (const struct TMH_RequestHandler *rh,
+ struct MHD_Connection *connection,
+ struct TMH_HandlerContext *hc)
+{
+ struct MfaState *mfa = hc->ctx;
+
+ if (NULL == mfa)
+ {
+ mfa = GNUNET_new (struct MfaState);
+ mfa->hc = hc;
+ hc->ctx = mfa;
+ hc->cc = &mfa_context_cleanup;
+ GNUNET_CONTAINER_DLL_insert (mfa_head,
+ mfa_tail,
+ mfa);
+ }
+
+ while (1)
+ {
+ GNUNET_log (GNUNET_ERROR_TYPE_INFO,
+ "Processing /challenge in phase %d\n",
+ (int) mfa->phase);
+ switch (mfa->phase)
+ {
+ case MFA_PHASE_PARSE:
+ phase_parse (mfa);
+ break;
+ case MFA_PHASE_LOOKUP:
+ phase_lookup (mfa);
+ break;
+ case MFA_PHASE_SENDING:
+ phase_send_challenge (mfa);
+ break;
+ case MFA_PHASE_SUSPENDING:
+ mfa->cwh = GNUNET_wait_child (mfa->child,
+ &transmission_done_cb,
+ mfa);
+ if (NULL == mfa->cwh)
+ {
+ respond_with_error (mfa,
+ MHD_HTTP_INTERNAL_SERVER_ERROR,
+ TALER_EC_GENERIC_ALLOCATION_FAILURE,
+ "GNUNET_wait_child");
+ continue;
+ }
+ mfa->suspended = GNUNET_YES;
+ MHD_suspend_connection (hc->connection);
+ return MHD_YES;
+ case MFA_PHASE_SENT:
+ phase_sent (mfa);
+ break;
+ case MFA_PHASE_RETURN_YES:
+ return MHD_YES;
+ case MFA_PHASE_RETURN_NO:
+ GNUNET_break (0);
+ return MHD_NO;
+ }
+ }
+}
diff --git a/src/backend/taler-merchant-httpd_post-challenge-ID.h b/src/backend/taler-merchant-httpd_post-challenge-ID.h
@@ -0,0 +1,49 @@
+/*
+ This file is part of TALER
+ (C) 2025 Taler Systems SA
+
+ TALER is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ TALER 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 General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+*/
+/**
+ * @file taler-merchant-httpd_post-challenge-ID.h
+ * @brief headers for POST /challenge/$ID handler
+ * @author Christian Grothoff
+ */
+#ifndef TALER_EXCHANGE_HTTPD_POST_CHALLENGE_ID_H
+#define TALER_EXCHANGE_HTTPD_POST_CHALLENGE_ID_H
+
+#include <microhttpd.h>
+#include "taler-merchant-httpd.h"
+
+
+/**
+ * Function to call when the HTTP server is shutting down to
+ * clean up all ongoing MFA challenge processes.
+ */
+void
+TMH_challenge_done (void);
+
+
+/**
+ * Transmit TAN code for multi-factor authentication to client.
+ *
+ * @param rh context of the handler
+ * @param connection the MHD connection to handle
+ * @param[in,out] hc context with further information about the request
+ * @return MHD result code
+ */
+MHD_RESULT
+TMH_post_challenge_ID (const struct TMH_RequestHandler *rh,
+ struct MHD_Connection *connection,
+ struct TMH_HandlerContext *hc);
+
+#endif