exchange

Base system with REST service to issue digital coins, run by the payment service provider
Log | Files | Refs | Submodules | README | LICENSE

commit 5b6579b711971cdc04e8435c65e1f041136d7ef5
parent ce42b6945114cd4a2518fdd01997aa76e06c98b5
Author: Özgür Kesim <oec@codeblau.de>
Date:   Tue, 15 Sep 2026 10:06:19 +0200

pq: array query parameter for unblinded denomination signatures

TALER_PQ_query_param_array_denom_sig() encodes each element exactly
like GNUNET_PQ_query_param_unblinded_sig(), so the elements can be read
back with GNUNET_PQ_result_spec_unblinded_sig().  test_pq inserts a CS
and an RSA signature and reads them back element-wise.

Diffstat:
Dsrc/exchangedb/do_insert_known_coin.c | 166-------------------------------------------------------------------------------
Dsrc/include/exchange-database/do_insert_known_coin.h | 98-------------------------------------------------------------------------------
Msrc/include/taler/taler_pq_lib.h | 18++++++++++++++++++
Msrc/pq/pq_common.h | 4++++
Msrc/pq/pq_query_helper.c | 97+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/pq/test_pq.c | 141++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
6 files changed, 259 insertions(+), 265 deletions(-)

diff --git a/src/exchangedb/do_insert_known_coin.c b/src/exchangedb/do_insert_known_coin.c @@ -1,166 +0,0 @@ -/* - This file is part of TALER - Copyright (C) 2022 Taler Systems SA - - TALER 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. - - TALER 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 - TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> - */ -/** - * @file exchangedb/do_insert_known_coin.c - * @brief Implementation of the do_insert_known_coin function for Postgres - * @author Christian Grothoff - */ -#include "exchangedb_lib.h" -#include "taler/taler_pq_lib.h" -#include "exchange-database/do_insert_known_coin.h" -#include "helper.h" - - -enum TALER_EXCHANGEDB_CoinKnownStatus -TALER_EXCHANGEDB_do_insert_known_coin ( - struct TALER_EXCHANGEDB_PostgresContext *pg, - const struct TALER_CoinPublicInfo *coin, - uint64_t *known_coin_id, - struct TALER_DenominationHashP *denom_hash, - struct TALER_AgeCommitmentHashP *h_age_commitment) -{ - enum GNUNET_DB_QueryStatus qs; - bool existed; - bool no_denom_pub_hash; - bool no_age_commitment_hash; - struct GNUNET_PQ_QueryParam params[] = { - GNUNET_PQ_query_param_auto_from_type (&coin->coin_pub), - GNUNET_PQ_query_param_auto_from_type (&coin->denom_pub_hash), - coin->no_age_commitment - ? GNUNET_PQ_query_param_null () - : GNUNET_PQ_query_param_auto_from_type (&coin->h_age_commitment), - TALER_PQ_query_param_denom_sig (&coin->denom_sig), - GNUNET_PQ_query_param_end - }; - struct GNUNET_PQ_ResultSpec rs[] = { - GNUNET_PQ_result_spec_bool ("existed", - &existed), - GNUNET_PQ_result_spec_uint64 ("known_coin_id", - known_coin_id), - GNUNET_PQ_result_spec_allow_null ( - GNUNET_PQ_result_spec_auto_from_type ("denom_pub_hash", - denom_hash), - &no_denom_pub_hash), - GNUNET_PQ_result_spec_allow_null ( - GNUNET_PQ_result_spec_auto_from_type ("age_commitment_hash", - h_age_commitment), - &no_age_commitment_hash), - GNUNET_PQ_result_spec_end - }; - - /* - See also: - https://stackoverflow.com/questions/34708509/how-to-use-returning-with-on-conflict-in-postgresql/37543015#37543015 - */ - PREPARE (pg, - "do_insert_known_coin", - "WITH dd" - " (denominations_serial" - " ,coin" - " ) AS (" - " SELECT " - " denominations_serial" - " ,coin" - " FROM denominations" - " WHERE denom_pub_hash=$2" - " ), input_rows" - " (coin_pub) AS (" - " VALUES ($1::BYTEA)" - " ), ins AS (" - " INSERT INTO known_coins " - " (coin_pub" - " ,denominations_serial" - " ,age_commitment_hash" - " ,denom_sig" - " ,remaining" - " ) SELECT " - " $1" - " ,denominations_serial" - " ,$3" - " ,$4" - " ,coin" - " FROM dd" - " ON CONFLICT DO NOTHING" /* CONFLICT on (coin_pub) */ - " RETURNING " - " known_coin_id" - " ) " - "SELECT " - " FALSE AS existed" - " ,known_coin_id" - " ,NULL AS denom_pub_hash" - " ,NULL AS age_commitment_hash" - " FROM ins " - "UNION ALL " - "SELECT " - " TRUE AS existed" - " ,known_coin_id" - " ,denom_pub_hash" - " ,kc.age_commitment_hash" - " FROM input_rows" - " JOIN known_coins kc USING (coin_pub)" - " JOIN denominations USING (denominations_serial)" - " LIMIT 1"); - qs = GNUNET_PQ_eval_prepared_singleton_select (pg->conn, - "do_insert_known_coin", - params, - rs); - switch (qs) - { - case GNUNET_DB_STATUS_HARD_ERROR: - GNUNET_break (0); - return TALER_EXCHANGEDB_CKS_HARD_FAIL; - case GNUNET_DB_STATUS_SOFT_ERROR: - return TALER_EXCHANGEDB_CKS_SOFT_FAIL; - case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: - GNUNET_break (0); /* should be impossible */ - return TALER_EXCHANGEDB_CKS_HARD_FAIL; - case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: - if (! existed) - return TALER_EXCHANGEDB_CKS_ADDED; - break; /* continued below */ - } - - if ( (! no_denom_pub_hash) && - (0 != GNUNET_memcmp (denom_hash, - &coin->denom_pub_hash)) ) - { - GNUNET_break_op (0); - return TALER_EXCHANGEDB_CKS_DENOM_CONFLICT; - } - - if (no_age_commitment_hash != coin->no_age_commitment) - { - if (no_age_commitment_hash) - { - GNUNET_break_op (0); - return TALER_EXCHANGEDB_CKS_AGE_CONFLICT_EXPECTED_NULL; - } - else - { - GNUNET_break_op (0); - return TALER_EXCHANGEDB_CKS_AGE_CONFLICT_EXPECTED_NON_NULL; - } - } - else if ( (! no_age_commitment_hash) && - (0 != GNUNET_memcmp (h_age_commitment, - &coin->h_age_commitment)) ) - { - GNUNET_break_op (0); - return TALER_EXCHANGEDB_CKS_AGE_CONFLICT_VALUE_DIFFERS; - } - - return TALER_EXCHANGEDB_CKS_PRESENT; -} diff --git a/src/include/exchange-database/do_insert_known_coin.h b/src/include/exchange-database/do_insert_known_coin.h @@ -1,98 +0,0 @@ -/* - This file is part of TALER - Copyright (C) 2022 Taler Systems SA - - TALER 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. - - TALER 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 - TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> - */ -/** - * @file src/include/exchange-database/do_insert_known_coin.h - * @brief implementation of the do_insert_known_coin function for Postgres - * @author Christian Grothoff - */ -#ifndef EXCHANGE_DATABASE_DO_INSERT_KNOWN_COIN_H -#define EXCHANGE_DATABASE_DO_INSERT_KNOWN_COIN_H - -#include "exchangedb_lib.h" - -/** - * Possible status codes from making sure a coin is known. - */ -enum TALER_EXCHANGEDB_CoinKnownStatus -{ - /** - * The coin was successfully added. - */ - TALER_EXCHANGEDB_CKS_ADDED = 1, - - /** - * The coin was already present. - */ - TALER_EXCHANGEDB_CKS_PRESENT = 0, - - /** - * Serialization failure. - */ - TALER_EXCHANGEDB_CKS_SOFT_FAIL = -1, - - /** - * Hard database failure. - */ - TALER_EXCHANGEDB_CKS_HARD_FAIL = -2, - - /** - * Conflicting coin (different denomination key) already in database. - */ - TALER_EXCHANGEDB_CKS_DENOM_CONFLICT = -3, - - /** - * Conflicting coin (expected NULL age hash) already in database. - */ - TALER_EXCHANGEDB_CKS_AGE_CONFLICT_EXPECTED_NULL = -4, - - /** - * Conflicting coin (unexpected NULL age hash) already in database. - */ - TALER_EXCHANGEDB_CKS_AGE_CONFLICT_EXPECTED_NON_NULL = -5, - - /** - * Conflicting coin (different age hash) already in database. - */ - TALER_EXCHANGEDB_CKS_AGE_CONFLICT_VALUE_DIFFERS = -6, - -}; - - -/** - * Make sure the given @a coin is known to the database. - * - * Primary test table: `known_coins` (see test_known_coins.c). - * - * @param pg the database context - * @param coin the coin that must be made known - * @param[out] known_coin_id set to the unique row of the coin - * @param[out] denom_hash set to the denomination hash of the existing - * coin (for conflict error reporting) - * @param[out] h_age_commitment set to the conflicting age commitment hash on conflict - * @return database transaction status, non-negative on success - */ -enum TALER_EXCHANGEDB_CoinKnownStatus -TALER_EXCHANGEDB_do_insert_known_coin (struct TALER_EXCHANGEDB_PostgresContext * - pg, - const struct TALER_CoinPublicInfo *coin, - uint64_t *known_coin_id, - struct TALER_DenominationHashP * - denom_hash, - struct TALER_AgeCommitmentHashP * - h_age_commitment) -; - -#endif diff --git a/src/include/taler/taler_pq_lib.h b/src/include/taler/taler_pq_lib.h @@ -154,6 +154,24 @@ TALER_PQ_query_param_array_blinded_denom_sig ( /** + * Generate query parameter for an array of (unblinded) denomination + * signatures. Each element is encoded exactly like + * #GNUNET_PQ_query_param_unblinded_sig() encodes a single signature, so + * the elements can be read back with #GNUNET_PQ_result_spec_unblinded_sig(). + * + * @param num number of elements in @a denom_sigs + * @param denom_sigs continuous array of denomination signatures + * @param db database context, needed for OID lookups + * @return query parameter to use + */ +struct GNUNET_PQ_QueryParam +TALER_PQ_query_param_array_denom_sig ( + size_t num, + const struct TALER_DenominationSignature *denom_sigs, + struct GNUNET_PQ_Context *db); + + +/** * Generate query parameter for an array of blinded hashes of coin envelopes * * @param num number of elements in @e denom_sigs diff --git a/src/pq/pq_common.h b/src/pq/pq_common.h @@ -42,6 +42,10 @@ enum TALER_PQ_ArrayType { TALER_PQ_array_of_blinded_denom_sig, + /** + * Unblinded denomination signatures (`struct TALER_DenominationSignature`). + */ + TALER_PQ_array_of_denom_sig, TALER_PQ_array_of_blinded_coin_hash, TALER_PQ_array_of_denom_hash, TALER_PQ_array_of_hash_code, diff --git a/src/pq/pq_query_helper.c b/src/pq/pq_query_helper.c @@ -785,6 +785,45 @@ qconv_array ( sizes = buffer_lengths; break; } + case TALER_PQ_array_of_denom_sig: + { + const struct TALER_DenominationSignature *denom_sigs = data; + size_t len; + + buffers = GNUNET_new_array (num, void *); + buffer_lengths = GNUNET_new_array (num, size_t); + + for (size_t i = 0; i<num; i++) + { + const struct GNUNET_CRYPTO_UnblindedSignature *ubs = + denom_sigs[i].unblinded_sig; + + switch (ubs->cipher) + { + case GNUNET_CRYPTO_BSA_RSA: + len = GNUNET_CRYPTO_rsa_signature_encode ( + ubs->details.rsa_signature, + &buffers[i]); + RETURN_UNLESS (len != 0); + break; + case GNUNET_CRYPTO_BSA_CS: + len = sizeof (ubs->details.cs_signature); + break; + default: + GNUNET_assert (0); + } + + /* for the cipher and marker */ + len += 2 * sizeof(uint32_t); + buffer_lengths[i] = len; + + y = total_size; + total_size += len; + RETURN_UNLESS (total_size >= y); + } + sizes = buffer_lengths; + break; + } default: GNUNET_assert (0); } @@ -925,6 +964,41 @@ qconv_array ( } break; } + case TALER_PQ_array_of_denom_sig: + { + const struct TALER_DenominationSignature *denom_sigs = data; + const struct GNUNET_CRYPTO_UnblindedSignature *ubs = + denom_sigs[i].unblinded_sig; + uint32_t be[2]; + + /* Same encoding as GNUNET_PQ_query_param_unblinded_sig() */ + be[0] = htonl ((uint32_t) ubs->cipher); + be[1] = htonl (0x00); /* magic marker: unblinded */ + GNUNET_memcpy (out, + &be, + sizeof(be)); + out += sizeof(be); + sz -= sizeof(be); + + switch (ubs->cipher) + { + case GNUNET_CRYPTO_BSA_RSA: + /* For RSA, 'same_sized' must have been false */ + GNUNET_assert (NULL != buffers); + GNUNET_memcpy (out, + buffers[i], + sz); + break; + case GNUNET_CRYPTO_BSA_CS: + GNUNET_memcpy (out, + &ubs->details.cs_signature, + sz); + break; + default: + GNUNET_assert (0); + } + break; + } case TALER_PQ_array_of_blinded_coin_hash: { const struct TALER_BlindedCoinHashP *coin_hs = data; @@ -1062,6 +1136,29 @@ TALER_PQ_query_param_array_blinded_denom_sig ( struct GNUNET_PQ_QueryParam +TALER_PQ_query_param_array_denom_sig ( + size_t num, + const struct TALER_DenominationSignature *denom_sigs, + struct GNUNET_PQ_Context *db) +{ + Oid oid; + + GNUNET_assert (GNUNET_OK == + GNUNET_PQ_get_oid_by_name (db, + "bytea", + &oid)); + return query_param_array_generic (num, + true, + denom_sigs, + NULL, + 0, + TALER_PQ_array_of_denom_sig, + oid, + NULL); +} + + +struct GNUNET_PQ_QueryParam TALER_PQ_query_param_array_blinded_coin_hash ( size_t num, const struct TALER_BlindedCoinHashP *coin_hs, diff --git a/src/pq/test_pq.c b/src/pq/test_pq.c @@ -44,8 +44,17 @@ postgres_prepare (struct GNUNET_PQ_Context *db) ",hash" ",hashes" ",cs_r_pubs" + ",denom_sigs" ") VALUES " - "($1, $2, $3, $4, $5, $6, $7, $8);"), + "($1, $2, $3, $4, $5, $6, $7, $8, $9);"), + GNUNET_PQ_make_prepare ("test_select_denom_sigs", + "SELECT" + " ds.denom_sig" + " FROM test_pq" + " CROSS JOIN LATERAL" + " UNNEST (denom_sigs) WITH ORDINALITY" + " AS ds (denom_sig, idx)" + " ORDER BY ds.idx;"), GNUNET_PQ_make_prepare ("test_select", "SELECT" " tamount" @@ -68,6 +77,79 @@ postgres_prepare (struct GNUNET_PQ_Context *db) /** + * Closure for #denom_sig_cb(). + */ +struct DenomSigCheck +{ + /** + * Signatures we inserted, in order. + */ + const struct TALER_DenominationSignature *expected; + + /** + * Number of entries in @e expected. + */ + unsigned int num; + + /** + * Rows seen so far. + */ + unsigned int seen; + + /** + * Set to true if a row did not match. + */ + bool mismatch; +}; + + +/** + * Compare each unblinded signature read back from the array + * with the one we inserted at the same position. + * + * @param cls a `struct DenomSigCheck *` + * @param result the result + * @param num_results number of rows + */ +static void +denom_sig_cb (void *cls, + PGresult *result, + unsigned int num_results) +{ + struct DenomSigCheck *dsc = cls; + + for (unsigned int i = 0; i < num_results; i++) + { + struct TALER_DenominationSignature got = { 0 }; + struct GNUNET_PQ_ResultSpec rs[] = { + GNUNET_PQ_result_spec_unblinded_sig ("denom_sig", + &got.unblinded_sig), + GNUNET_PQ_result_spec_end + }; + + if (GNUNET_OK != + GNUNET_PQ_extract_result (result, + rs, + i)) + { + GNUNET_break (0); + dsc->mismatch = true; + return; + } + if ( (dsc->seen >= dsc->num) || + (0 != TALER_denom_sig_cmp (&got, + &dsc->expected[dsc->seen])) ) + { + GNUNET_break (0); + dsc->mismatch = true; + } + dsc->seen++; + GNUNET_PQ_cleanup_result (rs); + } +} + + +/** * Run actual test queries. * * @return 0 on success @@ -75,6 +157,7 @@ postgres_prepare (struct GNUNET_PQ_Context *db) static int run_queries (struct GNUNET_PQ_Context *conn) { + struct TALER_DenominationSignature denom_sigs[2]; struct TALER_Amount tamount; struct TALER_Amount aamount[3]; struct TALER_Amount aamountc[2]; @@ -125,6 +208,31 @@ run_queries (struct GNUNET_PQ_Context *conn) json_integer (42))); GNUNET_CRYPTO_random_block (in_cs_r_pubs, sizeof(struct GNUNET_CRYPTO_CSPublicRPairP) * 5); + /* one CS and one RSA signature: the two encodings differ in size */ + { + struct GNUNET_CRYPTO_UnblindedSignature *ubs; + struct GNUNET_CRYPTO_RsaPrivateKey *rsa_priv; + + ubs = GNUNET_new (struct GNUNET_CRYPTO_UnblindedSignature); + ubs->cipher = GNUNET_CRYPTO_BSA_CS; + ubs->rc = 1; + GNUNET_CRYPTO_random_block (&ubs->details.cs_signature, + sizeof (ubs->details.cs_signature)); + denom_sigs[0].unblinded_sig = ubs; + + rsa_priv = GNUNET_CRYPTO_rsa_private_key_create (1024); + GNUNET_assert (NULL != rsa_priv); + ubs = GNUNET_new (struct GNUNET_CRYPTO_UnblindedSignature); + ubs->cipher = GNUNET_CRYPTO_BSA_RSA; + ubs->rc = 1; + ubs->details.rsa_signature + = GNUNET_CRYPTO_rsa_sign_fdh (rsa_priv, + &hc, + sizeof (hc)); + GNUNET_assert (NULL != ubs->details.rsa_signature); + GNUNET_CRYPTO_rsa_private_key_free (rsa_priv); + denom_sigs[1].unblinded_sig = ubs; + } { struct GNUNET_PQ_QueryParam params_insert[] = { TALER_PQ_query_param_amount (conn, @@ -147,6 +255,9 @@ run_queries (struct GNUNET_PQ_Context *conn) TALER_PQ_query_param_array_cs_r_pub (5, in_cs_r_pubs, conn), + TALER_PQ_query_param_array_denom_sig (2, + denom_sigs, + conn), GNUNET_PQ_query_param_end }; PGresult *result; @@ -288,6 +399,33 @@ run_queries (struct GNUNET_PQ_Context *conn) } GNUNET_PQ_cleanup_result (results_select); } + { + struct DenomSigCheck dsc = { + .expected = denom_sigs, + .num = 2 + }; + struct GNUNET_PQ_QueryParam params_select[] = { + GNUNET_PQ_query_param_end + }; + + if (2 != + GNUNET_PQ_eval_prepared_multi_select (conn, + "test_select_denom_sigs", + params_select, + &denom_sig_cb, + &dsc)) + { + GNUNET_break (0); + return 1; + } + GNUNET_break (2 == dsc.seen); + GNUNET_break (! dsc.mismatch); + if ( (2 != dsc.seen) || + (dsc.mismatch) ) + return 1; + } + TALER_denom_sig_free (&denom_sigs[0]); + TALER_denom_sig_free (&denom_sigs[1]); return 0; } @@ -337,6 +475,7 @@ main (int argc, ",hash gnunet_hashcode" ",hashes gnunet_hashcode[]" ",cs_r_pubs BYTEA[]" + ",denom_sigs BYTEA[]" ")"), GNUNET_PQ_EXECUTE_STATEMENT_END };