commit d70a8cbfbce6574b0a61f789962d49b5efb9891a
parent 977ff500b47ef6a4abcdbb08ef3a07747144c143
Author: Christian Grothoff <christian@grothoff.org>
Date: Sun, 31 Aug 2025 14:24:43 +0200
draft SQL functions for MFA challenges
Diffstat:
17 files changed, 1082 insertions(+), 7 deletions(-)
diff --git a/src/backenddb/Makefile.am b/src/backenddb/Makefile.am
@@ -39,6 +39,7 @@ sql_DATA = \
merchant-0020.sql \
merchant-0021.sql \
merchant-0022.sql \
+ merchant-0023.sql \
drop.sql
BUILT_SOURCES = \
@@ -133,6 +134,10 @@ libtaler_plugin_merchantdb_postgres_la_SOURCES = \
pg_insert_transfer.h pg_insert_transfer.c \
pg_insert_transfer_details.h pg_insert_transfer_details.c \
pg_insert_webhook.h pg_insert_webhook.c \
+ pg_lookup_mfa_challenge.h pg_lookup_mfa_challenge.c \
+ pg_solve_mfa_challenge.h pg_solve_mfa_challenge.c \
+ pg_update_mfa_challenge.h pg_update_mfa_challenge.c \
+ pg_create_mfa_challenge.h pg_create_mfa_challenge.c \
pg_lock_product.h pg_lock_product.c \
pg_lookup_account.h pg_lookup_account.c \
pg_lookup_all_products.h pg_lookup_all_products.c \
diff --git a/src/backenddb/future.sql b/src/backenddb/future.sql
@@ -1,13 +1,16 @@
+-- See enum TALER_MERCHANT_MFA_Channel
CREATE TYPE tan_enum
- AS ENUM ('sms', 'email');
+ AS ENUM ('sms', 'email', 'totp');
+-- See enum TALER_MERCHANT_MFA_CriticalOperation
CREATE TYPE op_enum
- AS ENUM ('account_provision', 'account_bank_reconfig', 'account_auth_reconfig', 'account_delete', 'create_token');
+ AS ENUM ('instance_provision', 'account_config', 'auth_config', 'instance_deletion', 'auth_token_creation');
CREATE TABLE tan_challenges
(challenge_id INT8 GENERATED BY DEFAULT AS IDENTITY UNIQUE
- ,body TEXT NOT NULL -- FIXME: use hash instead of plaintext, may contain passwords!
+ ,h_body BYTEA NOT NULL CHECK (LENGTH(h_body)=64)
+ ,salt BYTEA NOT NULL CHECK (LENGTH(salt)=16)
,op op_enum NOT NULL
,code TEXT NOT NULL
,creation_date INT8 NOT NULL
@@ -22,11 +25,17 @@ CREATE TABLE tan_challenges
,tan_info TEXT NULL DEFAULT NULL -- FIXME: when is this NULL?
);
COMMENT ON TABLE tan_challenges
- IS 'Stores 2FA challenges';
-COMMENT ON COLUMN tan_challenges.op
- IS 'The protected operation to run after the challenge';
+ IS 'Stores multi-factor authorization (MFA) challenges';
+COMMENT ON COLUMN tan_challenges.challenge_id
+ IS 'Unique identifier for the challenge';
COMMENT ON COLUMN tan_challenges.body
IS 'Request body of the original request that triggered the challenge, to be replayed once the challenge is satisfied. FIXME: probably should just store the hash of it.';
+COMMENT ON COLUMN tan_challenges.h_body
+ IS 'Salted hash of the body of the original request that triggered the challenge, to be replayed once the challenge is satisfied.';
+COMMENT ON COLUMN tan_challenges.salt
+ IS 'Salt used when hashing the original body.';
+COMMENT ON COLUMN tan_challenges.op
+ IS 'The protected operation to run after the challenge';
COMMENT ON COLUMN tan_challenges.code
IS 'The pin code sent to the user and verified';
COMMENT ON COLUMN tan_challenges.creation_date
@@ -44,6 +53,11 @@ COMMENT ON COLUMN tan_challenges.tan_channel
COMMENT ON COLUMN tan_challenges.tan_info
IS 'TAN information message to use to inform the user about where the challenge was sent';
+CREATE INDEX tan_challenges_lookup_index
+ ON tan_challenges (merchant_serial,op,expiration_date,creation_date DESC);
+COMMENT ON INDEX tan_challenges_lookup_index
+ IS 'for lookup_mfa_challenge collection';
+
CREATE INDEX tan_challenges_expiration_index
ON tan_challenges (expiration_date);
COMMENT ON INDEX tan_challenges_expiration_index
diff --git a/src/backenddb/merchant-0023.sql b/src/backenddb/merchant-0023.sql
@@ -0,0 +1,93 @@
+--
+-- This file is part of TALER
+-- Copyright (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 merchant-0023.sql
+-- @brief Create table to store MFA related information
+-- @author Christian Grothoff
+
+
+BEGIN;
+
+-- Check patch versioning is in place.
+SELECT _v.register_patch('merchant-0023', NULL, NULL);
+
+SET search_path TO merchant;
+
+
+-- See enum TALER_MERCHANT_MFA_Channel
+CREATE TYPE tan_enum
+ AS ENUM ('sms', 'email', 'totp');
+
+-- See enum TALER_MERCHANT_MFA_CriticalOperation
+CREATE TYPE op_enum
+ AS ENUM ('instance_provision', 'account_config', 'auth_config', 'instance_deletion', 'auth_token_creation');
+
+CREATE TABLE tan_challenges
+ (challenge_id INT8 GENERATED BY DEFAULT AS IDENTITY UNIQUE
+ ,h_body BYTEA NOT NULL CHECK (LENGTH(h_body)=64)
+ ,salt BYTEA NOT NULL CHECK (LENGTH(salt)=16)
+ ,op op_enum NOT NULL
+ ,code TEXT NOT NULL
+ ,creation_date INT8 NOT NULL
+ ,expiration_date INT8 NOT NULL
+ ,retransmission_date INT8 NOT NULL DEFAULT 0
+ ,confirmation_date INT8 DEFAULT NULL
+ ,retry_counter INT4 NOT NULL
+ ,merchant_serial INT8 NOT NULL
+ REFERENCES merchant_instances(merchant_serial)
+ ON DELETE CASCADE
+ ,tan_channel tan_enum NULL DEFAULT NULL
+ ,tan_info TEXT NULL DEFAULT NULL -- FIXME: when is this NULL?
+);
+COMMENT ON TABLE tan_challenges
+ IS 'Stores multi-factor authorization (MFA) challenges';
+COMMENT ON COLUMN tan_challenges.challenge_id
+ IS 'Unique identifier for the challenge';
+COMMENT ON COLUMN tan_challenges.h_body
+ IS 'Salted hash of the body of the original request that triggered the challenge, to be replayed once the challenge is satisfied.';
+COMMENT ON COLUMN tan_challenges.salt
+ IS 'Salt used when hashing the original body.';
+COMMENT ON COLUMN tan_challenges.op
+ IS 'The protected operation to run after the challenge';
+COMMENT ON COLUMN tan_challenges.code
+ IS 'The pin code sent to the user and verified';
+COMMENT ON COLUMN tan_challenges.creation_date
+ IS 'Creation date of the code';
+COMMENT ON COLUMN tan_challenges.retransmission_date
+ IS 'When did we last transmit the challenge to the user';
+COMMENT ON COLUMN tan_challenges.expiration_date
+ IS 'When will the code expire';
+COMMENT ON COLUMN tan_challenges.confirmation_date
+ IS 'When was this challenge successfully verified, NULL if pending';
+COMMENT ON COLUMN tan_challenges.retry_counter
+ IS 'How many tries are left for this code; must be > 0';
+COMMENT ON COLUMN tan_challenges.tan_channel
+ IS 'TAN channel to use, if NULL use customer configured one';
+COMMENT ON COLUMN tan_challenges.tan_info
+ IS 'TAN information message to use to inform the user about where the challenge was sent';
+
+CREATE INDEX tan_challenges_lookup_index
+ ON tan_challenges (merchant_serial,op,expiration_date,creation_date DESC);
+COMMENT ON INDEX tan_challenges_lookup_index
+ IS 'for lookup_mfa_challenge collection';
+
+CREATE INDEX tan_challenges_expiration_index
+ ON tan_challenges (expiration_date);
+COMMENT ON INDEX tan_challenges_expiration_index
+ IS 'for garbage collection';
+
+
+COMMIT;
diff --git a/src/backenddb/pg_create_mfa_challenge.c b/src/backenddb/pg_create_mfa_challenge.c
@@ -0,0 +1,91 @@
+/*
+ This file is part of TALER
+ Copyright (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 backenddb/pg_create_mfa_challenge.c
+ * @brief Implementation of the create_mfa_challenge function for Postgres
+ * @author Christian Grothoff
+ */
+#include "platform.h"
+#include <taler/taler_error_codes.h>
+#include <taler/taler_dbevents.h>
+#include <taler/taler_pq_lib.h>
+#include "pg_create_mfa_challenge.h"
+#include "pg_helper.h"
+
+
+enum GNUNET_DB_QueryStatus
+TMH_PG_create_mfa_challenge (
+ void *cls,
+ const char *instance_id,
+ enum TALER_MERCHANT_MFA_CriticalOperation op,
+ const struct TALER_MERCHANT_MFA_BodyHash *h_body,
+ const struct TALER_MERCHANT_MFA_BodySalt *salt,
+ const char *code,
+ struct GNUNET_TIME_Absolute expiration_date,
+ struct GNUNET_TIME_Absolute retransmission_date,
+ enum TALER_MERCHANT_MFA_Channel tan_channel,
+ const char *tan_info,
+ uint64_t *challenge_id)
+{
+ struct PostgresClosure *pg = cls;
+ struct GNUNET_TIME_Absolute now = GNUNET_TIME_absolute_get ();
+ const char *op_str = TALER_MERCHANT_MFA_co_to_string (op);
+ const char *channel_str = TALER_MERCHANT_MFA_channel_to_string (tan_channel);
+ struct GNUNET_PQ_QueryParam params[] = {
+ GNUNET_PQ_query_param_auto_from_type (h_body),
+ GNUNET_PQ_query_param_auto_from_type (salt),
+ GNUNET_PQ_query_param_string (op_str),
+ GNUNET_PQ_query_param_string (code),
+ GNUNET_PQ_query_param_absolute_time (&now), /* $5 */
+ GNUNET_PQ_query_param_absolute_time (&expiration_date),
+ 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_end
+ };
+ struct GNUNET_PQ_ResultSpec rs[] = {
+ GNUNET_PQ_result_spec_uint64 ("challenge_id",
+ challenge_id),
+ GNUNET_PQ_result_spec_end
+ };
+
+ PREPARE (pg,
+ "create_mfa_challenge",
+ "INSERT INTO tan_challenges"
+ " (h_body"
+ " ,salt"
+ " ,op"
+ " ,code"
+ " ,creation_date"
+ " ,expiration_date"
+ " ,retransmission_date"
+ " ,retry_counter"
+ " ,merchant_serial"
+ " ,tan_channel"
+ " ,tan_info)"
+ " SELECT"
+ " $1, $2, $3, $4, $5, $6, $7, 3, merchant_serial, $9, $10"
+ " FROM merchant_instances"
+ " WHERE merchant_id=$8"
+ " RETURNING challenge_id;");
+ return GNUNET_PQ_eval_prepared_singleton_select (pg->conn,
+ "create_mfa_challenge",
+ params,
+ rs);
+}
diff --git a/src/backenddb/pg_create_mfa_challenge.h b/src/backenddb/pg_create_mfa_challenge.h
@@ -0,0 +1,61 @@
+/*
+ This file is part of TALER
+ Copyright (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 backenddb/pg_create_mfa_challenge.h
+ * @brief implementation of the create_mfa_challenge function for Postgres
+ * @author Christian Grothoff
+ */
+#ifndef PG_CREATE_MFA_CHALLENGE_H
+#define PG_CREATE_MFA_CHALLENGE_H
+
+#include <taler/taler_util.h>
+#include <taler/taler_json_lib.h>
+#include "taler_merchantdb_plugin.h"
+
+
+/**
+ * Create new multi-factor authorization (MFA) challenge in the database.
+ *
+ * @param cls closure
+ * @param instance_id instance the operation effects
+ * @param op operation that triggered the MFA request
+ * @param h_body hash of the request body
+ * @param salt salt used to compute @a h_body
+ * @param code challenge code sent to the user
+ * @param expiration_date when should the challenge expire
+ * @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[out] challenge_id set to the ID of the new challenge
+ * @return database result code
+ */
+enum GNUNET_DB_QueryStatus
+TMH_PG_create_mfa_challenge (
+ void *cls,
+ const char *instance_id,
+ enum TALER_MERCHANT_MFA_CriticalOperation op,
+ const struct TALER_MERCHANT_MFA_BodyHash *h_body,
+ const struct TALER_MERCHANT_MFA_BodySalt *salt,
+ const char *code,
+ struct GNUNET_TIME_Absolute expiration_date,
+ struct GNUNET_TIME_Absolute retransmission_date,
+ enum TALER_MERCHANT_MFA_Channel tan_channel,
+ const char *tan_info,
+ uint64_t *challenge_id);
+
+#endif
diff --git a/src/backenddb/pg_lookup_mfa_challenge.c b/src/backenddb/pg_lookup_mfa_challenge.c
@@ -0,0 +1,228 @@
+/*
+ This file is part of TALER
+ Copyright (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 backenddb/pg_lookup_mfa_challenge.c
+ * @brief Implementation of the lookup_mfa_challenge function for Postgres
+ * @author Christian Grothoff
+ */
+#include "platform.h"
+#include <taler/taler_error_codes.h>
+#include <taler/taler_dbevents.h>
+#include <taler/taler_pq_lib.h>
+#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,
+ struct TALER_MERCHANT_MFA_BodySalt *salt,
+ 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)
+{
+ 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_end
+ };
+ enum GNUNET_DB_QueryStatus qs;
+
+ PREPARE (pg,
+ "lookup_mfa_challenge",
+ "SELECT "
+ " h_body"
+ " ,salt"
+ " ,confirmation_date"
+ " ,retransmission_date"
+ " ,retry_counter"
+ " ,tan_channel"
+ " ,challenge_id"
+ " FROM tan_challenges"
+ " WHERE merchant_serial IN"
+ " (SELECT merchant_serial"
+ " FROM merchant_instances"
+ " WHERE merchant_id = $1)"
+ " AND (tc.op = $2)"
+ " AND (expiration_date / 1000 / 1000"
+ " > EXTRACT(EPOCH FROM NOW()"
+ " AT TIME ZONE 'UTC')::bigint)"
+ " ORDER BY creation_date DESC");
+ /* Initialize to conservative values in case qs ends up <= 0 */
+ *tan_channel = TALER_MERCHANT_MFA_CHANNEL_NONE;
+ *retry_counter = 0;
+ *challenge_id = 0;
+ qs = GNUNET_PQ_eval_prepared_multi_select (pg->conn,
+ "lookup_mfa_challenge",
+ params,
+ &handle_mfa_challenge,
+ &ctx);
+ 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;
+ return qs;
+}
diff --git a/src/backenddb/pg_lookup_mfa_challenge.h b/src/backenddb/pg_lookup_mfa_challenge.h
@@ -0,0 +1,64 @@
+/*
+ This file is part of TALER
+ Copyright (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 backenddb/pg_lookup_mfa_challenge.h
+ * @brief implementation of the lookup_mfa_challenge function for Postgres
+ * @author Christian Grothoff
+ */
+#ifndef PG_LOOKUP_MFA_CHALLENGE_H
+#define PG_LOOKUP_MFA_CHALLENGE_H
+
+#include <taler/taler_util.h>
+#include <taler/taler_json_lib.h>
+#include "taler_merchantdb_plugin.h"
+
+
+/**
+ * Check if a new multi-factor authorization (MFA) challenge
+ * matching the body exists in the database.
+ *
+ * @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[out] salt salt used to compute @a h_body
+ * @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
+ * 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 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,
+ struct TALER_MERCHANT_MFA_BodySalt *salt,
+ 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);
+
+
+#endif
diff --git a/src/backenddb/pg_lookup_products.c b/src/backenddb/pg_lookup_products.c
@@ -150,6 +150,9 @@ TMH_PG_lookup_products (void *cls,
&plc);
/* If there was an error inside lookup_products_cb, return a hard error. */
if (plc.extract_failed)
+ {
+ GNUNET_break (0);
return GNUNET_DB_STATUS_HARD_ERROR;
+ }
return qs;
}
diff --git a/src/backenddb/pg_solve_mfa_challenge.c b/src/backenddb/pg_solve_mfa_challenge.c
@@ -0,0 +1,109 @@
+/*
+ This file is part of TALER
+ Copyright (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 backenddb/pg_solve_mfa_challenge.c
+ * @brief Implementation of the solve_mfa_challenge function for Postgres
+ * @author Christian Grothoff
+ */
+#include "platform.h"
+#include <taler/taler_error_codes.h>
+#include <taler/taler_dbevents.h>
+#include <taler/taler_pq_lib.h>
+#include "pg_solve_mfa_challenge.h"
+#include "pg_helper.h"
+
+
+enum GNUNET_DB_QueryStatus
+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)
+{
+ 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),
+ GNUNET_PQ_query_param_auto_from_type (h_body),
+ GNUNET_PQ_query_param_string (solution),
+ GNUNET_PQ_query_param_end
+ };
+ struct GNUNET_PQ_ResultSpec rs[] = {
+ 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;
+
+ /* conservatively set security-relevant return values to
+ safe values, even though these should not be used with qs <= 0 */
+ *solved = false;
+ *retry_counter = 0;
+ PREPARE (pg,
+ "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,
+ "solve_mfa_challenge",
+ params,
+ rs);
+ if (qs <= 0)
+ return qs;
+ if (no_match)
+ {
+ 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
@@ -0,0 +1,62 @@
+/*
+ This file is part of TALER
+ Copyright (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 backenddb/pg_solve_mfa_challenge.h
+ * @brief implementation of the solve_mfa_challenge function for Postgres
+ * @author Christian Grothoff
+ */
+#ifndef PG_SOLVE_MFA_CHALLENGE_H
+#define PG_SOLVE_MFA_CHALLENGE_H
+
+#include <taler/taler_util.h>
+#include <taler/taler_json_lib.h>
+#include "taler_merchantdb_plugin.h"
+
+/**
+ * Attempt to solve new multi-factor authorization (MFA) challenge.
+ * Checks the solution against the code in the database and updates
+ * the solution state and (on failure) retry counter depending on
+ * the result.
+ *
+ * @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
+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);
+
+#endif
diff --git a/src/backenddb/pg_solve_mfa_challenge.sql b/src/backenddb/pg_solve_mfa_challenge.sql
@@ -0,0 +1,91 @@
+--
+-- This file is part of TALER
+-- Copyright (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/>
+--
+
+CREATE OR REPLACE 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
+)
+LANGUAGE plpgsql
+AS $$
+DECLARE
+ my_instance_id INT8;
+DECLARE
+ my_rec RECORD;
+BEGIN
+ SELECT merchant_serial
+ INTO my_instance_id
+ FROM merchant_instances
+ WHERE merchant_id=in_instance_id;
+
+ -- 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
+ FROM tan_challenges tc
+ 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;
+
+ IF NOT FOUND
+ THEN
+ out_solved = FALSE;
+ RETURN;
+ END IF;
+
+ out_confirmation_date = my_rec.confirmation_date;
+ out_retransmission_date = my_rec.retransmission_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
+ THEN
+ out_solved := TRUE;
+ 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;
+ UPDATE tan_challenges
+ SET confirmation_date = out_confirmation_date
+ WHERE challenge_id = in_challenge_id;
+ ELSE
+ -- Failed to solve, decrement retry counter
+ out_retry_counter = out_retry_counter - 1;
+ UPDATE tan_challenges
+ SET retry_counter = out_retry_counter
+ WHERE challenge_id = in_challenge_id;
+ END IF;
+END;
+$$;
diff --git a/src/backenddb/pg_update_mfa_challenge.c b/src/backenddb/pg_update_mfa_challenge.c
@@ -0,0 +1,57 @@
+/*
+ This file is part of TALER
+ Copyright (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 backenddb/pg_update_mfa_challenge.c
+ * @brief Implementation of the update_mfa_challenge function for Postgres
+ * @author Christian Grothoff
+ */
+#include "platform.h"
+#include <taler/taler_error_codes.h>
+#include <taler/taler_dbevents.h>
+#include <taler/taler_pq_lib.h>
+#include "pg_update_mfa_challenge.h"
+#include "pg_helper.h"
+
+
+enum GNUNET_DB_QueryStatus
+TMH_PG_update_mfa_challenge (
+ void *cls,
+ uint64_t challenge_id,
+ const char *code,
+ uint32_t retry_counter,
+ struct GNUNET_TIME_Absolute retransmission_date)
+{
+ struct PostgresClosure *pg = cls;
+ struct GNUNET_PQ_QueryParam params[] = {
+ GNUNET_PQ_query_param_uint64 (&challenge_id),
+ GNUNET_PQ_query_param_string (code),
+ GNUNET_PQ_query_param_uint32 (&retry_counter),
+ GNUNET_PQ_query_param_absolute_time (&retransmission_date),
+ GNUNET_PQ_query_param_end
+ };
+
+ PREPARE (pg,
+ "update_mfa_challenge",
+ "UPDATE tan_challenges"
+ " SET"
+ " code=$2"
+ " ,retry_counter=$3"
+ " ,retransmission_date=$4"
+ " WHERE challenge_id = $1;");
+ return GNUNET_PQ_eval_prepared_non_select (pg->conn,
+ "update_mfa_challenge",
+ params);
+}
diff --git a/src/backenddb/pg_update_mfa_challenge.h b/src/backenddb/pg_update_mfa_challenge.h
@@ -0,0 +1,51 @@
+/*
+ This file is part of TALER
+ Copyright (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 backenddb/pg_update_mfa_challenge.h
+ * @brief implementation of the update_mfa_challenge function for Postgres
+ * @author Christian Grothoff
+ */
+#ifndef PG_UPDATE_MFA_CHALLENGE_H
+#define PG_UPDATE_MFA_CHALLENGE_H
+
+#include <taler/taler_util.h>
+#include <taler/taler_json_lib.h>
+#include "taler_merchantdb_plugin.h"
+
+
+/**
+ * Update the state of an MFA challenge as we have now
+ * retransmitted the challenge code.
+ *
+ * @param cls closure
+ * @param challenge_id challenge ID to be solved
+ * @param code new challenge code
+ * @param retry_counter number of attempts that remain
+ * for solving the challenge
+ * @param retransmission_date set to when a fresh challenge
+ * may be transmitted next
+ * @return database result code
+ */
+enum GNUNET_DB_QueryStatus
+TMH_PG_update_mfa_challenge (
+ void *cls,
+ uint64_t challenge_id,
+ const char *code,
+ uint32_t retry_counter,
+ struct GNUNET_TIME_Absolute retransmission_date);
+
+
+#endif
diff --git a/src/backenddb/plugin_merchantdb_postgres.c b/src/backenddb/plugin_merchantdb_postgres.c
@@ -63,6 +63,10 @@
#include "pg_lookup_instance_auth.h"
#include "pg_lookup_otp_devices.h"
#include "pg_update_transfer_status.h"
+#include "pg_lookup_mfa_challenge.h"
+#include "pg_solve_mfa_challenge.h"
+#include "pg_update_mfa_challenge.h"
+#include "pg_create_mfa_challenge.h"
#include "pg_finalize_transfer_status.h"
#include "pg_insert_instance.h"
#include "pg_account_kyc_set_status.h"
@@ -669,6 +673,14 @@ libtaler_plugin_merchantdb_postgres_init (void *cls)
= &TMH_PG_lookup_statistics_counter_by_interval;
plugin->lookup_statistics_amount_by_interval
= &TMH_PG_lookup_statistics_amount_by_interval;
+ plugin->lookup_mfa_challenge
+ = &TMH_PG_lookup_mfa_challenge;
+ plugin->solve_mfa_challenge
+ = &TMH_PG_solve_mfa_challenge;
+ plugin->update_mfa_challenge
+ = &TMH_PG_update_mfa_challenge;
+ plugin->create_mfa_challenge
+ = &TMH_PG_create_mfa_challenge;
plugin->gc
= &TMH_PG_gc;
#ifdef HAVE_DONAU_DONAU_SERVICE_H
diff --git a/src/backenddb/procedures.sql.in b/src/backenddb/procedures.sql.in
@@ -24,6 +24,7 @@ SET search_path TO merchant;
#include "pg_insert_spent_token.sql"
#include "pg_insert_transfer_details.sql"
#include "pg_update_product.sql"
+#include "pg_solve_mfa_challenge.sql"
#include "pg_account_kyc_set_status.sql"
#include "pg_account_kyc_set_failed.sql"
#include "pg_statistics_helpers.sql"
@@ -42,6 +43,11 @@ BEGIN
CALL merchant_statistic_amount_gc ();
CALL merchant_statistic_bucket_gc ();
CALL merchant_statistic_counter_gc ();
+
+ DELETE FROM tan_challenges
+ WHERE expiration_date / 1000 / 1000 <
+ EXTRACT(EPOCH FROM now()
+ AT TIME ZONE 'UTC')::bigint;
END $$;
COMMENT ON PROCEDURE merchant_do_gc
IS 'calls all other garbage collection subroutines';
diff --git a/src/include/taler_merchantdb_plugin.h b/src/include/taler_merchantdb_plugin.h
@@ -1,6 +1,6 @@
/*
This file is part of TALER
- Copyright (C) 2014-2024 Taler Systems SA
+ Copyright (C) 2014-2025 Taler Systems SA
TALER is free software; you can redistribute it and/or modify it under the
terms of the GNU Lesser General Public License as published by the Free Software
@@ -28,6 +28,7 @@
#include <gnunet/gnunet_util_lib.h>
#include <gnunet/gnunet_db_lib.h>
#include <taler/taler_exchange_service.h>
+#include <taler_merchant_util.h>
#include <jansson.h>
#ifdef HAVE_DONAU_DONAU_SERVICE_H
@@ -4165,6 +4166,132 @@ struct TALER_MERCHANTDB_Plugin
const struct TALER_Amount *new_amount
);
+
+ /**
+ * Check if a new matching multi-factor authorization (MFA) challenge
+ * exists in the database.
+ *
+ * @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[out] salt salt used to compute @a h_body
+ * @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
+ * 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 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,
+ struct TALER_MERCHANT_MFA_BodySalt *salt,
+ 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);
+
+
+ /**
+ * Attempt to solve new multi-factor authorization (MFA) challenge.
+ * Checks the solution against the code in the database and updates
+ * the solution state and (on failure) retry counter depending on
+ * the result.
+ *
+ * @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
+ (*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);
+
+
+ /**
+ * Update the state of an MFA challenge as we have now
+ * retransmitted the challenge code.
+ *
+ * @param cls closure
+ * @param challenge_id challenge ID to be solved
+ * @param code new challenge code
+ * @param retry_counter number of attempts that remain
+ * for solving the challenge
+ * @param retransmission_date set to when a fresh challenge
+ * may be transmitted next
+ * @return database result code
+ */
+ enum GNUNET_DB_QueryStatus
+ (*update_mfa_challenge)(
+ void *cls,
+ uint64_t challenge_id,
+ const char *code,
+ uint32_t retry_counter,
+ struct GNUNET_TIME_Absolute retransmission_date);
+
+
+ /**
+ * Create new multi-factor authorization (MFA) challenge in the database.
+ *
+ * @param cls closure
+ * @param instance_id instance the operation effects
+ * @param op operation that triggered the MFA request
+ * @param h_body hash of the request body
+ * @param salt salt used to compute @a h_body
+ * @param code challenge code sent to the user
+ * @param expiration_date when should the challenge expire
+ * @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[out] challenge_id set to the ID of the new challenge
+ * @return database result code
+ */
+ enum GNUNET_DB_QueryStatus
+ (*create_mfa_challenge)(
+ void *cls,
+ const char *instance_id,
+ enum TALER_MERCHANT_MFA_CriticalOperation op,
+ const struct TALER_MERCHANT_MFA_BodyHash *h_body,
+ const struct TALER_MERCHANT_MFA_BodySalt *salt,
+ const char *code,
+ struct GNUNET_TIME_Absolute expiration_date,
+ struct GNUNET_TIME_Absolute retransmission_date,
+ enum TALER_MERCHANT_MFA_Channel tan_channel,
+ const char *tan_info,
+ uint64_t *challenge_id);
+
+
#ifdef HAVE_DONAU_DONAU_SERVICE_H
/**
* Insert information about a Donau instance.
diff --git a/src/util/Makefile.am b/src/util/Makefile.am
@@ -42,6 +42,7 @@ libtalermerchantutil_la_SOURCES = \
contract_parse.c \
contract_serialize.c \
json.c \
+ mfa.c \
os_installation.c
libtalermerchantutil_la_LIBADD = \
-lgnunetjson \