commit 179962fc716237b48b1bda7f7c4c0860f4a95c39
parent 272abcdc610ebeffee241370e517f885b609a3a9
Author: Christian Grothoff <christian@grothoff.org>
Date: Tue, 4 Aug 2026 17:50:49 +0200
generate fresh payment_identifiers instead of re-using a client-supplied identifier and possibly violating a UNIQUE assumption; also introduce the implied UNIQUE constraint
Diffstat:
6 files changed, 103 insertions(+), 17 deletions(-)
diff --git a/src/backend/anastasis-httpd_policy-upload.c b/src/backend/anastasis-httpd_policy-upload.c
@@ -899,6 +899,7 @@ AH_handler_policy_post (
enum GNUNET_DB_QueryStatus qs;
qs = ANASTASIS_DB_get_recdoc_payment (
+ &puc->account,
&puc->payment_identifier,
&paid,
&valid_counter,
@@ -1186,24 +1187,24 @@ AH_handler_policy_post (
case ANASTASIS_DB_STORE_STATUS_STORE_LIMIT_EXCEEDED:
GNUNET_log (GNUNET_ERROR_TYPE_INFO,
"Storage request limit exceeded, requesting payment\n");
- if (! puc->payment_identifier_provided)
- {
- GNUNET_CRYPTO_random_block (&puc->payment_identifier,
- sizeof (struct ANASTASIS_PaymentSecretP));
- puc->payment_identifier_provided = true;
- GNUNET_log (GNUNET_ERROR_TYPE_INFO,
- "Also no payment identifier, requesting payment\n");
- }
+ /* This implies that a NEW payment is required, so the order we are
+ about to create must have a fresh ID. Re-using the identifier the
+ client supplied would ask the merchant for an order that already
+ exists, which would not be helpful. */
+ GNUNET_CRYPTO_random_block (&puc->payment_identifier,
+ sizeof (struct ANASTASIS_PaymentSecretP));
+ puc->payment_identifier_provided = true;
return begin_payment (puc);
case ANASTASIS_DB_STORE_STATUS_PAYMENT_REQUIRED:
GNUNET_log (GNUNET_ERROR_TYPE_INFO,
"Policy store operation requires payment\n");
- if (! puc->payment_identifier_provided)
- {
- GNUNET_CRYPTO_random_block (&puc->payment_identifier,
- sizeof (struct ANASTASIS_PaymentSecretP));
- puc->payment_identifier_provided = true;
- }
+ /* This again implies that a NEW payment is required, so the order we are
+ about to create must have a fresh ID. Re-using the identifier the
+ client supplied would ask the merchant for an order that already
+ exists, which would not be helpful. */
+ GNUNET_CRYPTO_random_block (&puc->payment_identifier,
+ sizeof (struct ANASTASIS_PaymentSecretP));
+ puc->payment_identifier_provided = true;
return begin_payment (puc);
case ANASTASIS_DB_STORE_STATUS_HARD_ERROR:
case ANASTASIS_DB_STORE_STATUS_SOFT_ERROR:
diff --git a/src/include/anastasis/anastasis-database/get_recdoc_payment.h b/src/include/anastasis/anastasis-database/get_recdoc_payment.h
@@ -30,13 +30,16 @@
* Check payment identifier. Used to check if a payment identifier given by
* the user is valid (existing and paid).
*
+ * @param account_pub account the payment must belong to
* @param payment_secret payment secret which the user must provide with every upload
* @param[out] paid bool value to show if payment is paid
* @param[out] valid_counter bool value to show if post_counter is > 0
+ * @param[out] creation_date when was the payment record created
* @return transaction status
*/
enum GNUNET_DB_QueryStatus
ANASTASIS_DB_get_recdoc_payment (
+ const struct ANASTASIS_CRYPTO_AccountPublicKeyP *account_pub,
const struct ANASTASIS_PaymentSecretP *payment_secret,
bool *paid,
bool *valid_counter,
diff --git a/src/stasis/get_recdoc_payment.c b/src/stasis/get_recdoc_payment.c
@@ -30,6 +30,7 @@
* Check payment identifier. Used to check if a payment identifier given by
* the user is valid (existing and paid).
*
+ * @param account_pub account the payment must belong to
* @param payment_secret payment secret which the user must provide with every upload
* @param[out] paid bool value to show if payment is paid
* @param[out] valid_counter bool value to show if post_counter is > 0
@@ -38,12 +39,14 @@
*/
enum GNUNET_DB_QueryStatus
ANASTASIS_DB_get_recdoc_payment (
+ const struct ANASTASIS_CRYPTO_AccountPublicKeyP *account_pub,
const struct ANASTASIS_PaymentSecretP *payment_secret,
bool *paid,
bool *valid_counter,
struct GNUNET_TIME_Timestamp *creation_date)
{
struct GNUNET_PQ_QueryParam params[] = {
+ GNUNET_PQ_query_param_auto_from_type (account_pub),
GNUNET_PQ_query_param_auto_from_type (payment_secret),
GNUNET_PQ_query_param_end
};
@@ -58,6 +61,11 @@ ANASTASIS_DB_get_recdoc_payment (
};
*creation_date = GNUNET_TIME_UNIT_ZERO_TS;
+ /* The payment identifier is chosen by the client, so it is only meaningful
+ together with the account it was issued for: without the user_id an
+ identifier belonging to one account would be accepted as credit for
+ another, and a lookup would answer with a row that the caller then goes
+ on to treat as its own. */
PREPARE ("get_recdoc_payment",
"SELECT"
" creation_date"
@@ -65,7 +73,8 @@ ANASTASIS_DB_get_recdoc_payment (
",amount"
",paid"
" FROM anastasis_recdoc_payment"
- " WHERE payment_identifier=$1;");
+ " WHERE user_id=$1"
+ " AND payment_identifier=$2;");
return GNUNET_PQ_eval_prepared_singleton_select (pg->conn,
"get_recdoc_payment",
params,
diff --git a/src/stasis/meson.build b/src/stasis/meson.build
@@ -7,6 +7,7 @@ install_data(
'versioning.sql',
'stasis-0001.sql',
'stasis-0002.sql',
+ 'stasis-0003.sql',
'drop.sql',
install_dir: sqldir,
)
diff --git a/src/stasis/stasis-0003.sql b/src/stasis/stasis-0003.sql
@@ -0,0 +1,57 @@
+--
+-- This file is part of Anastasis
+-- Copyright (C) 2026 Anastasis SARL
+--
+-- ANASTASIS 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.
+--
+-- ANASTASIS 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
+-- ANASTASIS; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+--
+
+-- Everything in one big transaction
+BEGIN;
+
+-- Check patch versioning is in place.
+SELECT _v.register_patch('stasis-0003', NULL, NULL);
+
+SET search_path TO anastasis;
+
+-- ANASTASIS_DB_get_recdoc_payment() is a singleton select on
+-- (user_id, payment_identifier): a second row for the same pair does not make
+-- it return the first one, it makes it return a hard error, i.e. the account
+-- can no longer use that identifier at all. Nothing enforced the uniqueness
+-- the readers assume, and the backend could be talked into inserting a
+-- duplicate by re-using a payment identifier for an order the merchant
+-- answers idempotently. The C side no longer does that; this index is what
+-- makes it impossible rather than merely unlikely.
+--
+-- Duplicates that already exist have to go first, or the index cannot be
+-- built. Of a duplicate group we keep the paid row if there is one (that is
+-- the row carrying the account's credit) and otherwise the oldest, which is
+-- the one the client was told about; the rest are unpaid leftovers of the
+-- situation described above and are exactly what garbage collection would
+-- have removed anyway.
+DELETE FROM anastasis_recdoc_payment
+ WHERE payment_id NOT IN (
+ SELECT DISTINCT ON (user_id, payment_identifier) payment_id
+ FROM anastasis_recdoc_payment
+ ORDER BY user_id
+ ,payment_identifier
+ ,paid DESC
+ ,creation_date ASC
+ ,payment_id ASC);
+
+CREATE UNIQUE INDEX anastasis_recdoc_payment_identifier_unique
+ ON anastasis_recdoc_payment
+ (user_id
+ ,payment_identifier);
+COMMENT ON INDEX anastasis_recdoc_payment_identifier_unique
+ IS 'A payment identifier identifies at most one payment per account; the readers are singleton selects on this pair';
+
+COMMIT;
diff --git a/src/stasis/test_anastasis_db.c b/src/stasis/test_anastasis_db.c
@@ -158,15 +158,16 @@ run (void *cls)
1));
}
+ memset (&accountPubP, 2, sizeof (accountPubP));
+ memset (&accountSig, 3, sizeof (accountSig));
FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS !=
ANASTASIS_DB_get_recdoc_payment (
+ &accountPubP,
&paymentSecretP,
&paid,
&valid_counter,
&pi_creation_date));
- memset (&accountPubP, 2, sizeof (accountPubP));
- memset (&accountSig, 3, sizeof (accountSig));
FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
ANASTASIS_DB_do_insert_recdoc_payment (
&accountPubP,
@@ -208,10 +209,24 @@ run (void *cls)
}
FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
ANASTASIS_DB_get_recdoc_payment (
+ &accountPubP,
&paymentSecretP,
&paid,
&valid_counter,
&pi_creation_date));
+ {
+ /* The same identifier under a different account must not be visible. */
+ struct ANASTASIS_CRYPTO_AccountPublicKeyP otherPubP;
+
+ memset (&otherPubP, 7, sizeof (otherPubP));
+ FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS !=
+ ANASTASIS_DB_get_recdoc_payment (
+ &otherPubP,
+ &paymentSecretP,
+ &paid,
+ &valid_counter,
+ &pi_creation_date));
+ }
FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS !=
ANASTASIS_DB_get_challenge_payment (
&paymentSecretP,