commit 1ff51f2de5dea26ecd3b1fbf8bd1b30ec464fe85
parent c9a48b069b7884dfc260aa811ddb4170113b223d
Author: Christian Grothoff <christian@grothoff.org>
Date: Tue, 28 Jul 2026 16:18:14 +0200
Fix #11692
Diffstat:
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