merchant

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

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:
Msrc/backenddb/Makefile.am | 5+++++
Msrc/backenddb/future.sql | 26++++++++++++++++++++------
Asrc/backenddb/merchant-0023.sql | 93+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/backenddb/pg_create_mfa_challenge.c | 91+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/backenddb/pg_create_mfa_challenge.h | 61+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/backenddb/pg_lookup_mfa_challenge.c | 228+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/backenddb/pg_lookup_mfa_challenge.h | 64++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/backenddb/pg_lookup_products.c | 3+++
Asrc/backenddb/pg_solve_mfa_challenge.c | 109+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/backenddb/pg_solve_mfa_challenge.h | 62++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/backenddb/pg_solve_mfa_challenge.sql | 91+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/backenddb/pg_update_mfa_challenge.c | 57+++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/backenddb/pg_update_mfa_challenge.h | 51+++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/backenddb/plugin_merchantdb_postgres.c | 12++++++++++++
Msrc/backenddb/procedures.sql.in | 6++++++
Msrc/include/taler_merchantdb_plugin.h | 129++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
Msrc/util/Makefile.am | 1+
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 \