merchant

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

commit 1ff51f2de5dea26ecd3b1fbf8bd1b30ec464fe85
parent c9a48b069b7884dfc260aa811ddb4170113b223d
Author: Christian Grothoff <christian@grothoff.org>
Date:   Tue, 28 Jul 2026 16:18:14 +0200

Fix #11692

Diffstat:
Mmeson.build | 2+-
Msrc/backend/taler-merchant-httpd_post-orders-ORDER_ID-pay.c | 11++++++-----
Msrc/backend/taler-merchant-httpd_post-private-orders.c | 42++++++++++++++++++++++++++++++++++++++++--
Msrc/backenddb/meson.build | 1+
Asrc/backenddb/update_token_family_key_expiration.c | 68++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/include/merchant-database/all.h | 1+
Asrc/include/merchant-database/update_token_family_key_expiration.h | 58++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
7 files changed, 175 insertions(+), 8 deletions(-)

diff --git a/meson.build b/meson.build @@ -338,7 +338,7 @@ if not get_option('only-doc') ['libtalermerchant', '9:0:1'], ['libtalermerchantutil', '1:1:1'], ['libtalermerchantbank', '0:1:0'], - ['libtalermerchantdb', '5:0:0'], + ['libtalermerchantdb', '6:0:1'], ['libtalermerchanttesting', '4:1:1'], ] diff --git a/src/backend/taler-merchant-httpd_post-orders-ORDER_ID-pay.c b/src/backend/taler-merchant-httpd_post-orders-ORDER_ID-pay.c @@ -3773,11 +3773,12 @@ handle_output_token (struct PayContext *pc, GNUNET_free (details.token_family.cipher_spec); if (NULL == details.priv.private_key) { - /* The key SHOULD have been created when we - created the order (after all, the client was able - to blind, and that requires us having had a public - key). So how did we loose it? Bad bug, this should - not be possible. */ + /* The key must exist: the LEFT JOIN in get_token_family_key() + only yields a NULL private key if no key covers the validity + period *and* survives until the pay deadline, and POST /orders + guarantees exactly that before it commits the contract terms + (it extends the retention of an existing key or mints a new + one, see #11692). Kept as a safety net. */ GNUNET_break (0); pay_end (pc, TALER_MHD_reply_with_error ( diff --git a/src/backend/taler-merchant-httpd_post-private-orders.c b/src/backend/taler-merchant-httpd_post-private-orders.c @@ -60,6 +60,7 @@ #include "merchant-database/get_order_summary.h" #include "merchant-database/get_product.h" #include "merchant-database/get_token_family_key.h" +#include "merchant-database/update_token_family_key_expiration.h" #include "merchant-database/iterate_token_family_keys.h" #include "merchant-database/iterate_donau_instances_filtered.h" #include "merchant-database/get_otp_device.h" @@ -1745,8 +1746,10 @@ create_key (const char *cipher_spec, * Check if the token family with the given @a slug is already present in the * list of token families for this order. If not, fetch its details and add it * to the list. Also checks if there is a public key with that expires after - * the payment deadline. If not, generates a new key pair and stores it in - * the database. + * the payment deadline. If a key covering @a valid_at exists but its private + * key would be deleted before the payment deadline, the lifetime of that + * private key is extended; only if no key covers @a valid_at at all do we + * generate a new key pair and store it in the database. * * @param[in,out] oc order context * @param slug slug of the token family @@ -1765,6 +1768,41 @@ add_output_token_family (struct OrderContext *oc, struct TALER_MERCHANT_ContractTokenFamily *family; enum GNUNET_DB_QueryStatus qs; + /* We are about to promise a token of this family, so the private key + covering @a valid_at must survive until we sign at the pay deadline. If + an existing key covers the validity period but was minted for an order + with an earlier pay deadline, extend its lifetime instead of minting a + second key for the very same validity period: the key lookups below (and + find_key_index()) would otherwise consider that key missing and we would + end up listing two keys for one validity period in the contract terms. */ + qs = TALER_MERCHANTDB_update_token_family_key_expiration ( + TMH_db, + oc->hc->instance->settings.id, + slug, + valid_at, + oc->parse_order.order->pay_deadline); + switch (qs) + { + case GNUNET_DB_STATUS_HARD_ERROR: + GNUNET_break (0); + reply_with_error (oc, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_STORE_FAILED, + "update_token_family_key_expiration"); + return GNUNET_SYSERR; + case GNUNET_DB_STATUS_SOFT_ERROR: + /* Single-statement transaction shouldn't possibly cause serialization errors. + Thus treating like a hard error. */ + GNUNET_break (0); + reply_with_error (oc, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_SOFT_FAILURE, + "update_token_family_key_expiration"); + return GNUNET_SYSERR; + default: + /* No key needed extending, or one/more were extended; either is fine. */ + break; + } family = find_family (oc, slug); if ( (NULL != family) && diff --git a/src/backenddb/meson.build b/src/backenddb/meson.build @@ -184,6 +184,7 @@ libtalermerchantdb = library( 'update_product.c', 'update_template.c', 'update_token_family.c', + 'update_token_family_key_expiration.c', 'update_expected_transfer_status.c', 'update_to_expected_transfer_finalized.c', 'delete_donau_instance.c', diff --git a/src/backenddb/update_token_family_key_expiration.c b/src/backenddb/update_token_family_key_expiration.c @@ -0,0 +1,68 @@ +/* + This file is part of TALER + Copyright (C) 2026 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 src/backenddb/update_token_family_key_expiration.c + * @brief Implementation of the update_token_family_key_expiration function for Postgres + * @author Christian Grothoff + */ +#include "platform.h" +#include <gnunet/gnunet_pq_lib.h> +#include <taler/taler_pq_lib.h> +#include "merchant-database/update_token_family_key_expiration.h" +#include "helper.h" + + +enum GNUNET_DB_QueryStatus +TALER_MERCHANTDB_update_token_family_key_expiration ( + struct TALER_MERCHANTDB_PostgresContext *pg, + const char *instance_id, + const char *token_family_slug, + struct GNUNET_TIME_Timestamp valid_at, + struct GNUNET_TIME_Timestamp sign_until) +{ + struct GNUNET_PQ_QueryParam params[] = { + GNUNET_PQ_query_param_string (token_family_slug), + GNUNET_PQ_query_param_timestamp (&valid_at), + GNUNET_PQ_query_param_timestamp (&sign_until), + GNUNET_PQ_query_param_end + }; + enum GNUNET_DB_QueryStatus qs; + + GNUNET_assert (NULL != pg->current_merchant_id); + GNUNET_assert (0 == strcmp (instance_id, + pg->current_merchant_id)); + TMH_PQ_prepare_anon (pg, + "UPDATE merchant_token_family_keys mtfk" + " SET private_key_deleted_at=" + " GREATEST (mtfk.private_key_deleted_at,$3)" + " FROM merchant_token_families mtf" + " WHERE (mtf.token_family_serial = mtfk.token_family_serial)" + " AND (mtf.slug=$1)" + " AND ($2 >= mtfk.signature_validity_start)" + " AND ($2 <= mtfk.signature_validity_end)" + " AND ($3 > mtfk.private_key_deleted_at)" + " AND (mtfk.priv IS NOT NULL)"); + qs = GNUNET_PQ_eval_prepared_non_select (pg->conn, + "", + params); + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Extending MTFK %s valid at %llu to sign until %llu got %d\n", + token_family_slug, + (unsigned long long) valid_at.abs_time.abs_value_us, + (unsigned long long) sign_until.abs_time.abs_value_us, + (int) qs); + return qs; +} diff --git a/src/include/merchant-database/all.h b/src/include/merchant-database/all.h @@ -196,6 +196,7 @@ #include "merchant-database/update_report_status.h" #include "merchant-database/update_template.h" #include "merchant-database/update_token_family.h" +#include "merchant-database/update_token_family_key_expiration.h" #include "merchant-database/update_expected_transfer_status.h" #include "merchant-database/update_unit.h" #include "merchant-database/update_webhook.h" diff --git a/src/include/merchant-database/update_token_family_key_expiration.h b/src/include/merchant-database/update_token_family_key_expiration.h @@ -0,0 +1,58 @@ +/* + This file is part of TALER + Copyright (C) 2026 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 src/include/merchant-database/update_token_family_key_expiration.h + * @brief implementation of the update_token_family_key_expiration function for Postgres + * @author Christian Grothoff + */ +#ifndef MERCHANT_DATABASE_UPDATE_TOKEN_FAMILY_KEY_EXPIRATION_H +#define MERCHANT_DATABASE_UPDATE_TOKEN_FAMILY_KEY_EXPIRATION_H + +#include <gnunet/gnunet_common.h> +#include <taler/taler_util.h> +#include <taler/taler_json_lib.h> +#include "merchantdb_lib.h" + + +/** + * Extend the lifetime of the private key(s) of the token family + * @a token_family_slug that cover the validity period @a valid_at, so that + * they can still be used to sign tokens until @a sign_until. Keys whose + * private key already lives at least that long (and keys whose private key + * was already deleted) are left alone. + * + * This is what keeps the backend from minting a second key for a validity + * period that is already covered, just because a later order has a pay + * deadline beyond the retention deadline of the existing key. + * + * @param pg database context + * @param instance_id instance to update the token family key for + * @param token_family_slug slug of the token family to update + * @param valid_at only extend keys with a validity period that includes this time + * @param sign_until the private key must be usable for signing until this time + * @return database result code, #GNUNET_DB_STATUS_SUCCESS_NO_RESULTS if no + * key needed extending + */ +enum GNUNET_DB_QueryStatus +TALER_MERCHANTDB_update_token_family_key_expiration ( + struct TALER_MERCHANTDB_PostgresContext *pg, + const char *instance_id, + const char *token_family_slug, + struct GNUNET_TIME_Timestamp valid_at, + struct GNUNET_TIME_Timestamp sign_until); + + +#endif