commit c41e94915ffc7c7d441164df57485787d7262f3e
parent da8729819a0a91edd5f5d4ad8c801b609d2ac615
Author: Christian Grothoff <christian@grothoff.org>
Date: Fri, 7 Aug 2026 00:59:20 +0200
handle corner-case where security module gives us a known key, but the amount now differs from what we had configured for that key (true defense in depth: ignore change, keep using old amount, bitch to admin to create a new key for new amount)
Diffstat:
4 files changed, 152 insertions(+), 6 deletions(-)
diff --git a/src/donau/donau-httpd_get-keys.c b/src/donau/donau-httpd_get-keys.c
@@ -29,6 +29,7 @@
#include "donau-httpd_get-keys.h"
#include "donau-httpd_get-config.h"
#include "donaudb_lib.h"
+#include "donau-database/get_donation_unit_amount.h"
#include "donau-database/insert_donation_unit.h"
#include "donau-database/insert_signkey.h"
#include "donau-database/iterate_donation_units.h"
@@ -651,6 +652,57 @@ DH_keys_finished ()
/**
+ * Reconcile a donation unit the security module just re-announced with the
+ * row we already have for it in the database.
+ *
+ * The row is authoritative and immutable: `receipts_submitted` references
+ * `h_donation_unit_pub`, and #DONAUDB_get_receipts_submitted_total() sums
+ * `donation_units.value` over those references, so the stored value is what
+ * every donation statement issued under this key was and will be computed
+ * from. If the configuration meanwhile says something else, we must adopt
+ * the stored value here -- otherwise /keys advertises one amount while
+ * /donation-statement keeps counting another one, permanently.
+ *
+ * @param[in,out] du in-memory donation unit to reconcile
+ * @param section_name configuration section @a du's value was read from
+ * @return #GNUNET_OK on success, #GNUNET_SYSERR on database failure
+ */
+static enum GNUNET_GenericReturnValue
+adopt_stored_donation_unit (struct DH_DonationUnitKey *du,
+ const char *section_name)
+{
+ struct TALER_Amount stored;
+ enum GNUNET_DB_QueryStatus qs;
+ char *cfg_value;
+
+ qs = DONAUDB_get_donation_unit_amount (DH_context,
+ &du->h_donation_unit_pub,
+ &stored);
+ if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != qs)
+ {
+ /* The INSERT told us the row exists, so it must be readable. */
+ GNUNET_break (0);
+ return GNUNET_SYSERR;
+ }
+ if (0 == TALER_amount_cmp (&stored,
+ &du->value))
+ return GNUNET_OK;
+ /* TALER_amount2s() returns a single static buffer, so we cannot use it
+ twice in one log statement. */
+ cfg_value = GNUNET_strdup (TALER_amount2s (&du->value));
+ GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
+ "Configuration section `%s' sets VALUE to %s, but donation unit %s is already stored with %s. Keeping the stored value, as donation statements issued under this key are computed from it. To donate at a different value, have the security module generate a new key.\n",
+ section_name,
+ cfg_value,
+ GNUNET_h2s (&du->h_donation_unit_pub.hash),
+ TALER_amount2s (&stored));
+ GNUNET_free (cfg_value);
+ du->value = stored;
+ return GNUNET_OK;
+}
+
+
+/**
* Function called with information about available keys for signing. Usually
* only called once per key upon connect. Also called again in case a key is
* being revoked, in that case with an @a end_time of zero.
@@ -744,9 +796,23 @@ helper_rsa_cb (
DH_global_ret = EXIT_FAILURE;
return;
}
+ if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs)
+ {
+ /* Key was already stored (the helper re-announces on every restart). */
+ if (GNUNET_OK !=
+ adopt_stored_donation_unit (du,
+ section_name))
+ {
+ GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
+ "Failed to look up stored donation unit\n");
+ GNUNET_SCHEDULER_shutdown ();
+ DH_global_ret = EXIT_FAILURE;
+ return;
+ }
+ }
GNUNET_log (GNUNET_ERROR_TYPE_INFO,
- "Inserted RSA donation unit of %s\n",
- TALER_amount2s (&value));
+ "Using RSA donation unit of %s\n",
+ TALER_amount2s (&du->value));
key_generation++;
}
@@ -845,9 +911,23 @@ helper_cs_cb (
DH_global_ret = EXIT_FAILURE;
return;
}
+ if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs)
+ {
+ /* Key was already stored (the helper re-announces on every restart). */
+ if (GNUNET_OK !=
+ adopt_stored_donation_unit (du,
+ section_name))
+ {
+ GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
+ "Failed to look up stored donation unit\n");
+ GNUNET_SCHEDULER_shutdown ();
+ DH_global_ret = EXIT_FAILURE;
+ return;
+ }
+ }
GNUNET_log (GNUNET_ERROR_TYPE_INFO,
- "Inserted CS donation unit of %s\n",
- TALER_amount2s (&value));
+ "Using CS donation unit of %s\n",
+ TALER_amount2s (&du->value));
key_generation++;
}
diff --git a/src/donaudb/0002-donation_units.sql b/src/donaudb/0002-donation_units.sql
@@ -22,7 +22,7 @@ CREATE TABLE donation_units
,value taler_amount NOT NULL
);
COMMENT ON TABLE donation_units
- IS 'Main donation_unit table. All the valid donation units the Donau knows about.';
+ IS 'Main donation_unit table. All the valid donation units the Donau knows about. Rows are immutable: receipts_submitted references h_donation_unit_pub and donation statements are summed from value, so a stored row must keep describing what was signed under that key. There is deliberately no UNIQUE constraint on (validity_year, value): when a key is replaced (e.g. the security module lost the private key), the replacement is stored alongside the old one, which stays valid for the signatures already made with it.';
COMMENT ON COLUMN donation_units.h_donation_unit_pub
IS 'Hash value of the donation unit public key.';
COMMENT ON COLUMN donation_units.validity_year
diff --git a/src/donaudb/insert_donation_unit.c b/src/donaudb/insert_donation_unit.c
@@ -45,6 +45,19 @@ DONAUDB_insert_donation_unit (struct DONAUDB_PostgresContext *ctx,
GNUNET_PQ_query_param_end
};
+ /* A donation_units row is a historical fact, not a mirror of the current
+ configuration: receipts_submitted references h_donation_unit_pub and
+ DONAUDB_get_receipts_submitted_total() sums donation_units.value over
+ those references. Rewriting validity_year or value here would therefore
+ retroactively change donation statements we have already issued under
+ that key -- so an existing row is never touched. The security module
+ re-announces every key on each restart, and h_donation_unit_pub covers
+ the key material only, so this conflict is the normal case, not an
+ error. An operator who needs a different year or value must have the
+ security module produce a *new* key; the schema deliberately has no
+ UNIQUE constraint on (validity_year, value), so the new unit coexists
+ with the old one, which stays valid for the signatures already made
+ with it. */
PREPARE (ctx,
"insert_donation_unit",
"INSERT INTO donation_units "
@@ -52,7 +65,8 @@ DONAUDB_insert_donation_unit (struct DONAUDB_PostgresContext *ctx,
",donation_unit_pub"
",validity_year"
",value"
- ") VALUES ($1, $2, $3, $4);");
+ ") VALUES ($1, $2, $3, $4)"
+ " ON CONFLICT (h_donation_unit_pub) DO NOTHING;");
return GNUNET_PQ_eval_prepared_non_select (ctx->conn,
"insert_donation_unit",
iparams);
diff --git a/src/donaudb/test_donaudb.c b/src/donaudb/test_donaudb.c
@@ -653,6 +653,58 @@ run (void *cls)
&donation_unit_info_cb,
NULL));
+ /* D-9: the security module re-announces every donation unit on each
+ restart, and h_donation_unit_pub covers the key material only. A
+ donation_units row is a historical fact -- receipts already issued under
+ this key are summed from its value -- so a re-announce with a different
+ year and value must leave the stored row untouched. */
+ {
+ struct TALER_Amount other_value;
+ struct TALER_Amount stored_value;
+
+ GNUNET_assert (GNUNET_OK ==
+ TALER_string_to_amount (CURRENCY ":2.000020",
+ &other_value));
+ FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS !=
+ DONAUDB_insert_donation_unit (ctx,
+ &h_donation_unit_pub,
+ &du_pub,
+ validity_year + 1,
+ &other_value));
+ FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
+ DONAUDB_get_donation_unit_amount (ctx,
+ &h_donation_unit_pub,
+ &stored_value));
+ FAILIF (0 != TALER_amount_cmp (&stored_value,
+ &du_value));
+ }
+
+ /* D-9: a key the operator retires is replaced by a *new* key, which may
+ well carry the same year and value. Both must be storable side by side,
+ so that the signatures already made with the old one stay accountable. */
+ {
+ struct TALER_DenominationPrivateKey second_priv;
+ struct TALER_DenominationPublicKey second_pub;
+ struct DONAU_DonationUnitPublicKey second_du_pub;
+ struct DONAU_DonationUnitHashP second_h_du_pub;
+
+ RND_BLK (&second_h_du_pub);
+ GNUNET_assert (GNUNET_OK ==
+ TALER_denom_priv_create (&second_priv,
+ &second_pub,
+ GNUNET_CRYPTO_BSA_RSA,
+ RSA_KEY_SIZE));
+ second_du_pub.bsign_pub_key = second_pub.bsign_pub_key;
+ FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
+ DONAUDB_insert_donation_unit (ctx,
+ &second_h_du_pub,
+ &second_du_pub,
+ validity_year,
+ &du_value));
+ TALER_denom_priv_free (&second_priv);
+ TALER_denom_pub_free (&second_pub);
+ }
+
TALER_denom_pub_free (&denom_pub);
/* test insert signing key */