donau

Donation authority for GNU Taler (experimental)
Log | Files | Refs | Submodules | README | LICENSE

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:
Msrc/donau/donau-httpd_get-keys.c | 88+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----
Msrc/donaudb/0002-donation_units.sql | 2+-
Msrc/donaudb/insert_donation_unit.c | 16+++++++++++++++-
Msrc/donaudb/test_donaudb.c | 52++++++++++++++++++++++++++++++++++++++++++++++++++++
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 */