donau

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

commit 0da2155fc1017a211eb4d33cc5a0bc550c41d8b4
parent b82f0ef5b50227008e319785c616194ab3497b8e
Author: Christian Grothoff <christian@grothoff.org>
Date:   Fri,  7 Aug 2026 00:21:59 +0200

do not return 201 if we had a conflict

Diffstat:
Msrc/donau/donau-httpd_get-config.h | 7++++++-
Msrc/donau/donau-httpd_post-batch-submit.c | 23++++++++++++++++++++---
Msrc/donaudb/insert_receipts_submitted.c | 11+++++++++--
Msrc/donaudb/test_donaudb.c | 139+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/include/donau-database/insert_receipts_submitted.h | 8+++++++-
5 files changed, 181 insertions(+), 7 deletions(-)

diff --git a/src/donau/donau-httpd_get-config.h b/src/donau/donau-httpd_get-config.h @@ -41,7 +41,12 @@ * * Returned via both /config and /keys endpoints. */ -#define DONAU_PROTOCOL_VERSION "1:0:1" +/* Revision bumped from 0 to 1: POST /batch-submit now answers 409 + DONAU_DONOR_IDENTIFIER_NONCE_REUSE for receipts it cannot store, + where it used to answer 201 Created and drop them. `current' is + unchanged: no request or response schema changed, and a client that + never reuses a nonce sees no difference. */ +#define DONAU_PROTOCOL_VERSION "1:1:1" /** diff --git a/src/donau/donau-httpd_post-batch-submit.c b/src/donau/donau-httpd_post-batch-submit.c @@ -246,6 +246,7 @@ DH_handler_post_batch_submit (struct DH_RequestContext *rc, { enum GNUNET_DB_QueryStatus qs; + size_t conflict_index = num_dr; /* Concurrent submissions can serialize against each other; the database reports that as a soft error, which means "retry me". */ @@ -257,7 +258,8 @@ DH_handler_post_batch_submit (struct DH_RequestContext *rc, &irc.h_donor_tax_id, num_dr, irc.donation_receipts, - irc.donation_year); + irc.donation_year, + &conflict_index); if (GNUNET_DB_STATUS_SOFT_ERROR != qs) break; } @@ -275,9 +277,24 @@ DH_handler_post_batch_submit (struct DH_RequestContext *rc, TALER_EC_GENERIC_DB_STORE_FAILED, "insert_receipts_submitted"); } + /* `receipts_submitted.nonce' is globally unique, so a nonce this donor + already used for a *different* donation unit is refused by the + database. That receipt was NOT stored -- reporting 201 Created for + it silently loses a tax-relevant donation. */ + if (conflict_index < num_dr) + { + char idx_str[24]; + + GNUNET_snprintf (idx_str, + sizeof (idx_str), + "%llu", + (unsigned long long) conflict_index); + return TALER_MHD_reply_with_error (rc->connection, + MHD_HTTP_CONFLICT, + TALER_EC_DONAU_DONOR_IDENTIFIER_NONCE_REUSE, + idx_str); + } } - /* Note: we do not care here about conflicts on insert, - as conflicts only happen for idempotent requests. */ GNUNET_log (GNUNET_ERROR_TYPE_INFO, "submitted receipts inserted!\n"); diff --git a/src/donaudb/insert_receipts_submitted.c b/src/donaudb/insert_receipts_submitted.c @@ -34,7 +34,8 @@ DONAUDB_insert_receipts_submitted ( struct DONAU_HashDonorTaxId *h_donor_tax_id, size_t num_dr, const struct DONAU_DonationReceipt donation_receipts[static num_dr], - uint64_t donation_year) + uint64_t donation_year, + size_t *conflict_index) { struct GNUNET_HashCode h_donation_unit_pubs[GNUNET_NZL (num_dr)]; struct DONAU_UniqueDonorIdentifierNonce nonces[GNUNET_NZL (num_dr)]; @@ -63,6 +64,7 @@ DONAUDB_insert_receipts_submitted ( }; enum GNUNET_DB_QueryStatus qs; + *conflict_index = num_dr; for (unsigned int i = 0; i < num_dr; i++) { const struct DONAU_DonationReceipt *dr = &donation_receipts[i]; @@ -89,11 +91,16 @@ DONAUDB_insert_receipts_submitted ( { for (size_t i = 0; i < num_dr; i++) { + /* NOTE: `conflicted[i]' is TRUE only for the *non*-idempotent case, + i.e. the row was not inserted AND the stored row is not this + donor's. That receipt was dropped and the caller must be told. */ if (conflicted[i]) { GNUNET_log (GNUNET_ERROR_TYPE_WARNING, - "Submitted donation receipt at index %llu already present!\n", + "Submitted donation receipt at index %llu conflicts with an existing entry!\n", (unsigned long long) i); + if (num_dr == *conflict_index) + *conflict_index = i; } } } diff --git a/src/donaudb/test_donaudb.c b/src/donaudb/test_donaudb.c @@ -149,6 +149,78 @@ donation_unit_info_cb ( /** + * Create an unblinded donation unit signature. The database layer only + * serializes it, so any well-formed signature will do. + * + * @return freshly allocated unblinded signature + */ +static struct GNUNET_CRYPTO_UnblindedSignature * +make_unblinded_sig (void) +{ + struct GNUNET_CRYPTO_UnblindedSignature *us; + struct GNUNET_HashCode msg; + + if (NULL == test_denom_priv.bsign_priv_key) + GNUNET_assert (GNUNET_OK == + TALER_denom_priv_create (&test_denom_priv, + &test_denom_pub, + GNUNET_CRYPTO_BSA_RSA, + RSA_KEY_SIZE)); + RND_BLK (&msg); + us = GNUNET_new (struct GNUNET_CRYPTO_UnblindedSignature); + us->cipher = GNUNET_CRYPTO_BSA_RSA; + us->rc = 1; + us->details.rsa_signature + = GNUNET_CRYPTO_rsa_sign_fdh ( + test_denom_priv.bsign_priv_key->details.rsa_private_key, + &msg, + sizeof (msg)); + GNUNET_assert (NULL != us->details.rsa_signature); + return us; +} + + +/** + * Register a donation unit with a random key hash. + * + * @param[out] h_donation_unit_pub set to the hash identifying the unit + * @param year validity year of the unit + * @param value value of the unit + * @return database status of the operation + */ +static enum GNUNET_DB_QueryStatus +make_donation_unit (struct DONAU_DonationUnitHashP *h_donation_unit_pub, + uint64_t year, + const char *value) +{ + struct TALER_DenominationPrivateKey dpriv; + struct TALER_DenominationPublicKey dpub; + struct DONAU_DonationUnitPublicKey du_pub; + struct TALER_Amount val; + enum GNUNET_DB_QueryStatus qs; + + RND_BLK (h_donation_unit_pub); + GNUNET_assert (GNUNET_OK == + TALER_denom_priv_create (&dpriv, + &dpub, + GNUNET_CRYPTO_BSA_RSA, + RSA_KEY_SIZE)); + du_pub.bsign_pub_key = dpub.bsign_pub_key; + GNUNET_assert (GNUNET_OK == + TALER_string_to_amount (value, + &val)); + qs = DONAUDB_insert_donation_unit (ctx, + h_donation_unit_pub, + &du_pub, + year, + &val); + TALER_denom_priv_free (&dpriv); + TALER_denom_pub_free (&dpub); + return qs; +} + + +/** * Function called with information about the donau's online signing keys. * * @param cls NULL @@ -678,6 +750,73 @@ run (void *cls) GNUNET_free (second_meta.charity_url); } + /* D-7: `receipts_submitted.nonce' is globally unique, so a donor who + reuses one UDI nonce for a *different* donation unit in a second + request has that receipt silently dropped. The DB layer must tell + the caller which index was refused, so /batch-submit can answer 409 + instead of 201. */ + { + struct DONAU_HashDonorTaxId donor; + struct DONAU_DonationUnitHashP du_a; + struct DONAU_DonationUnitHashP du_b; + struct DONAU_DonationReceipt dr; + struct DONAU_UniqueDonorIdentifierNonce nonce; + struct TALER_Amount total; + struct TALER_Amount expected; + size_t conflict_index = 42; + uint64_t year = GNUNET_TIME_get_current_year (); + + RND_BLK (&donor); + RND_BLK (&nonce); + FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != + make_donation_unit (&du_a, + year, + CURRENCY ":10")); + FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != + make_donation_unit (&du_b, + year, + CURRENCY ":25")); + + dr.h_donation_unit_pub = du_a; + dr.nonce = nonce; + dr.donation_unit_sig.unblinded_sig = make_unblinded_sig (); + FAILIF (0 > + DONAUDB_insert_receipts_submitted (ctx, + &donor, + 1, + &dr, + year, + &conflict_index)); + FAILIF (1 != conflict_index); /* == num_dr, i.e. no conflict */ + GNUNET_CRYPTO_unblinded_sig_decref (dr.donation_unit_sig.unblinded_sig); + + /* Same donor, same nonce, different donation unit: refused. */ + dr.h_donation_unit_pub = du_b; + dr.nonce = nonce; + dr.donation_unit_sig.unblinded_sig = make_unblinded_sig (); + FAILIF (0 > + DONAUDB_insert_receipts_submitted (ctx, + &donor, + 1, + &dr, + year, + &conflict_index)); + FAILIF (0 != conflict_index); + GNUNET_CRYPTO_unblinded_sig_decref (dr.donation_unit_sig.unblinded_sig); + + /* Only the first receipt is deductible; the second one was dropped. */ + FAILIF (0 > + DONAUDB_get_receipts_submitted_total (ctx, + year, + &donor, + &total)); + GNUNET_assert (GNUNET_OK == + TALER_string_to_amount (CURRENCY ":10", + &expected)); + FAILIF (0 != TALER_amount_cmp (&total, + &expected)); + } + result = 0; drop: diff --git a/src/include/donau-database/insert_receipts_submitted.h b/src/include/donau-database/insert_receipts_submitted.h @@ -32,6 +32,11 @@ * @param num_dr number of donation receipts * @param donation_receipts array of donation receipts * @param donation_year year of the donation + * @param[out] conflict_index set to the index of the first receipt that + * could NOT be stored because a *different* receipt with the + * same nonce already exists, or to @a num_dr if all receipts + * were stored (or were idempotent repetitions of this donor's + * own receipts) * @return transaction status code */ enum GNUNET_DB_QueryStatus @@ -40,6 +45,7 @@ DONAUDB_insert_receipts_submitted ( struct DONAU_HashDonorTaxId *h_donor_tax_id, size_t num_dr, const struct DONAU_DonationReceipt donation_receipts[static num_dr], - uint64_t donation_year); + uint64_t donation_year, + size_t *conflict_index); #endif