anastasis

Credential backup and recovery protocol and service
Log | Files | Refs | Submodules | README | LICENSE

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:
Msrc/backend/anastasis-httpd_policy-upload.c | 29+++++++++++++++--------------
Msrc/include/anastasis/anastasis-database/get_recdoc_payment.h | 3+++
Msrc/stasis/get_recdoc_payment.c | 11++++++++++-
Msrc/stasis/meson.build | 1+
Asrc/stasis/stasis-0003.sql | 57+++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/stasis/test_anastasis_db.c | 19+++++++++++++++++--
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,