exchange

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

commit ba515b48f858ab79867a08369ad2fc8aaf31e93a
parent 3d5a97c36b35c4a9d5a854dcbca5842274bcf5bf
Author: Özgür Kesim <oec@codeblau.de>
Date:   Tue, 15 Sep 2026 21:09:33 +0200

lib: verify the coin conflict proofs of 409 responses

Following the vCONFLICT spec, the denomination conflict reply now
carries the stored age commitment hash and the age commitment conflict
reply the stored denomination signature, so both can be verified with
the coin data alone.

The client library parses these replies into
struct TALER_EXCHANGE_CoinConflict and checks them in
TALER_EXCHANGE_check_coin_conflict_(): the denomination resp. age
commitment named by the exchange must differ from what the client
used, and the exchange's signature must verify against the
denomination from /keys.  If that denomination is not in /keys, the
reply is accepted as unverifiable (verified=false).  Batch deposit,
melt, purse create, purse deposit, reserve open and both recoup
clients run the check and expose the details in their results; a
failed check yields TALER_EC_GENERIC_REPLY_MALFORMED.  The details
are released only for a 409 with one of the two error codes, as they
share a union with the details of other replies.  The unimplemented
stub and the unused helpers are removed.

The JSON parsers for denomination public keys now fill pub_key_hash,
so parsed keys compare equal to locally created ones.

Fixes issue: https://bugs.taler.net/n/9422

Diffstat:
Mmeson.build | 2+-
Msrc/exchange/taler-exchange-httpd_db.c | 51++++++++++++++++++++++++++++++++++++++++++---------
Msrc/exchange/taler-exchange-httpd_responses.c | 16+++++++++++-----
Msrc/exchange/taler-exchange-httpd_responses.h | 10++++++++--
Msrc/include/exchange-database/do_insert_known_coins.h | 3+--
Msrc/include/taler/exchange/common.h | 138+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/include/taler/exchange/post-batch-deposit.h | 28++++++++--------------------
Msrc/include/taler/exchange/post-melt.h | 10++++++++++
Msrc/include/taler/exchange/post-purses-PURSE_PUB-create.h | 10++++++++++
Msrc/include/taler/exchange/post-purses-PURSE_PUB-deposit.h | 11+++++++++++
Msrc/include/taler/exchange/post-recoup-refresh.h | 11+++++++++++
Msrc/include/taler/exchange/post-recoup-withdraw.h | 11+++++++++++
Msrc/include/taler/exchange/post-reserves-RESERVE_PUB-open.h | 7+++++++
Msrc/json/json_helper.c | 10++++++++++
Msrc/lib/exchange_api_common.c | 311++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---
Msrc/lib/exchange_api_common.h | 208+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----------
Msrc/lib/exchange_api_get-coins-COIN_PUB-history.c | 52----------------------------------------------------
Msrc/lib/exchange_api_post-batch-deposit.c | 93++++++++++++++++++++++++++++++++++++++++++-------------------------------------
Msrc/lib/exchange_api_post-melt.c | 63+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/lib/exchange_api_post-purses-PURSE_PUB-create.c | 51++++++++++++++++++++++++++++++++++++++++++++++++++-
Msrc/lib/exchange_api_post-purses-PURSE_PUB-deposit.c | 50++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/lib/exchange_api_post-recoup-refresh.c | 70++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--
Msrc/lib/exchange_api_post-recoup-withdraw.c | 70++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--
Msrc/lib/exchange_api_post-reserves-RESERVE_PUB-open.c | 72++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/lib/meson.build | 22++++++++++++++++++++++
Asrc/lib/test_coin_conflict.c | 599+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/testing/testing_api_cmd_deposit.c | 19+++++++++++++++++++
Msrc/testing/testing_api_cmd_refresh.c | 18++++++++++++++++++
28 files changed, 1843 insertions(+), 173 deletions(-)

diff --git a/meson.build b/meson.build @@ -278,7 +278,7 @@ if not get_option('only-doc') libltversions = [ ['libtalerutil', '18:0:4'], - ['libtalerjson', '10:0:6'], + ['libtalerjson', '10:1:6'], ['libtalercurl', '1:0:1'], ['libtalerpq', '2:0:1'], ['libtalermhd', '8:0:1'], diff --git a/src/exchange/taler-exchange-httpd_db.c b/src/exchange/taler-exchange-httpd_db.c @@ -134,7 +134,10 @@ TEH_make_coins_known ( TALER_EC_EXCHANGE_GENERIC_COIN_CONFLICTING_DENOMINATION_KEY, &coin->coin_pub, &prev_denom.pub, - &prev_denom.sig); + &prev_denom.sig, + res->no_age_commitment + ? NULL + : &res->h_age_commitment); TALER_denom_pub_free (&prev_denom.pub); TALER_denom_sig_free (&prev_denom.sig); return GNUNET_DB_STATUS_HARD_ERROR; @@ -142,14 +145,44 @@ TEH_make_coins_known ( case TALER_EXCHANGEDB_CKS_AGE_CONFLICT_EXPECTED_NULL: case TALER_EXCHANGEDB_CKS_AGE_CONFLICT_EXPECTED_NON_NULL: case TALER_EXCHANGEDB_CKS_AGE_CONFLICT_VALUE_DIFFERS: - *mhd_ret = TEH_RESPONSE_reply_coin_age_commitment_conflict ( - connection, - TALER_EC_EXCHANGE_GENERIC_COIN_CONFLICTING_AGE_HASH, - res->status, - &res->h_denom_pub, - &coin->coin_pub, - &res->h_age_commitment); - return GNUNET_DB_STATUS_HARD_ERROR; + /* The exchange has seen this coin before, with a different age + * commitment. Get the stored denomination signature (which covers + * the stored age commitment hash) and send it to the client as + * proof */ + { + struct + { + struct TALER_DenominationPublicKey pub; + struct TALER_DenominationSignature sig; + } prev_denom = {0}; + + if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != + TALER_EXCHANGEDB_get_signature_for_known_coin (TEH_pg, + &coin->coin_pub, + &prev_denom.pub, + &prev_denom.sig)) + { + /* There _should_ have been a result, because + * we ended here due to a conflict! */ + GNUNET_break (0); + *mhd_ret = TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + NULL); + return GNUNET_DB_STATUS_HARD_ERROR; + } + *mhd_ret = TEH_RESPONSE_reply_coin_age_commitment_conflict ( + connection, + TALER_EC_EXCHANGE_GENERIC_COIN_CONFLICTING_AGE_HASH, + res->status, + &res->h_denom_pub, + &coin->coin_pub, + &res->h_age_commitment, + &prev_denom.sig); + TALER_denom_pub_free (&prev_denom.pub); + TALER_denom_sig_free (&prev_denom.sig); + return GNUNET_DB_STATUS_HARD_ERROR; + } } GNUNET_assert (0); return GNUNET_DB_STATUS_HARD_ERROR; diff --git a/src/exchange/taler-exchange-httpd_responses.c b/src/exchange/taler-exchange-httpd_responses.c @@ -199,7 +199,8 @@ TEH_RESPONSE_reply_coin_denomination_conflict ( enum TALER_ErrorCode ec, const struct TALER_CoinSpendPublicKeyP *coin_pub, const struct TALER_DenominationPublicKey *prev_denom_pub, - const struct TALER_DenominationSignature *prev_denom_sig) + const struct TALER_DenominationSignature *prev_denom_sig, + const struct TALER_AgeCommitmentHashP *prev_h_age_commitment) { return TALER_MHD_REPLY_JSON_PACK ( connection, @@ -210,9 +211,11 @@ TEH_RESPONSE_reply_coin_denomination_conflict ( TALER_JSON_pack_denom_pub ("prev_denom_pub", prev_denom_pub), TALER_JSON_pack_denom_sig ("prev_denom_sig", - prev_denom_sig) + prev_denom_sig), + GNUNET_JSON_pack_allow_null ( + GNUNET_JSON_pack_data_auto ("prev_h_age_commitment", + prev_h_age_commitment)) ); - } @@ -223,7 +226,8 @@ TEH_RESPONSE_reply_coin_age_commitment_conflict ( enum TALER_EXCHANGEDB_CoinKnownStatus status, const struct TALER_DenominationHashP *h_denom_pub, const struct TALER_CoinSpendPublicKeyP *coin_pub, - const struct TALER_AgeCommitmentHashP *h_age_commitment) + const struct TALER_AgeCommitmentHashP *h_age_commitment, + const struct TALER_DenominationSignature *prev_denom_sig) { const char *conflict_detail; @@ -256,7 +260,9 @@ TEH_RESPONSE_reply_coin_age_commitment_conflict ( GNUNET_JSON_pack_data_auto ("expected_age_commitment_hash", h_age_commitment)), GNUNET_JSON_pack_string ("conflict_detail", - conflict_detail) + conflict_detail), + TALER_JSON_pack_denom_sig ("prev_denom_sig", + prev_denom_sig) ); } diff --git a/src/exchange/taler-exchange-httpd_responses.h b/src/exchange/taler-exchange-httpd_responses.h @@ -166,6 +166,8 @@ TEH_RESPONSE_reply_coin_insufficient_funds ( * @param coin_pub the public key of the coin * @param prev_denom_pub the denomination of the coin, as seen previously * @param prev_denom_sig the signature with the denomination key over the coin + * @param prev_h_age_commitment age commitment hash of the coin as seen + * previously (covered by @a prev_denom_sig), NULL if none * @return MHD result code */ enum MHD_Result @@ -174,7 +176,8 @@ TEH_RESPONSE_reply_coin_denomination_conflict ( enum TALER_ErrorCode ec, const struct TALER_CoinSpendPublicKeyP *coin_pub, const struct TALER_DenominationPublicKey *prev_denom_pub, - const struct TALER_DenominationSignature *prev_denom_sig); + const struct TALER_DenominationSignature *prev_denom_sig, + const struct TALER_AgeCommitmentHashP *prev_h_age_commitment); /** * Send the salted hash of the merchant's bank account from conflicting @@ -205,6 +208,8 @@ TEH_RESPONSE_reply_coin_conflicting_contract ( * @param h_denom_pub hash of the denomination of the coin * @param coin_pub public key of the coin * @param h_age_commitment hash of the age commitment as found in the database + * @param prev_denom_sig the denomination signature of the coin as found in + * the database (covers @a coin_pub and @a h_age_commitment) * @return MHD result code */ enum MHD_Result @@ -214,7 +219,8 @@ TEH_RESPONSE_reply_coin_age_commitment_conflict ( enum TALER_EXCHANGEDB_CoinKnownStatus cks, const struct TALER_DenominationHashP *h_denom_pub, const struct TALER_CoinSpendPublicKeyP *coin_pub, - const struct TALER_AgeCommitmentHashP *h_age_commitment); + const struct TALER_AgeCommitmentHashP *h_age_commitment, + const struct TALER_DenominationSignature *prev_denom_sig); /** * Fundamental details about a purse. diff --git a/src/include/exchange-database/do_insert_known_coins.h b/src/include/exchange-database/do_insert_known_coins.h @@ -93,8 +93,7 @@ struct TALER_EXCHANGEDB_CoinKnownResult /** * Denomination hash of the coin as stored in the database. For a - * conflict, this is what the client must be told. Not valid for the - * age conflicts. + * conflict, this is what the client must be told. */ struct TALER_DenominationHashP h_denom_pub; diff --git a/src/include/taler/exchange/common.h b/src/include/taler/exchange/common.h @@ -184,6 +184,144 @@ TALER_EXCHANGE_parse_451 (struct TALER_EXCHANGE_KycNeededRedirect *uflr, /** + * Details of a #TALER_EC_EXCHANGE_GENERIC_COIN_CONFLICTING_DENOMINATION_KEY + * response: the exchange knows the coin under a different denomination + * and proves it with the denomination signature it holds for the coin. + * The library verifies the proof before passing it to the application + * (see @e verified). + */ +struct TALER_EXCHANGE_CoinDenominationConflict +{ + /** + * Public key of the coin. + */ + struct TALER_CoinSpendPublicKeyP coin_pub; + + /** + * Denomination under which the exchange knows the coin. + * Differs from the one the client used. + */ + struct TALER_DenominationPublicKey prev_denom_pub; + + /** + * Hash of @e prev_denom_pub. + */ + struct TALER_DenominationHashP prev_h_denom_pub; + + /** + * Signature by @e prev_denom_pub over the coin (and + * @e prev_h_age_commitment, if any). + */ + struct TALER_DenominationSignature prev_denom_sig; + + /** + * Age commitment hash the exchange stored for the coin. + * Only valid if @e no_prev_age_commitment is false. + */ + struct TALER_AgeCommitmentHashP prev_h_age_commitment; + + /** + * True if the exchange stored the coin without an age + * commitment hash (@e prev_denom_pub is not age-restricted). + */ + bool no_prev_age_commitment; + + /** + * True if @e prev_denom_sig was verified against a denomination + * from our exchange keys. False if @e prev_denom_pub is not + * among the denominations we know (for example because it expired + * and was dropped from /keys); the response is then accepted as + * unverifiable. + */ + bool verified; +}; + + +/** + * Details of a #TALER_EC_EXCHANGE_GENERIC_COIN_CONFLICTING_AGE_HASH + * response: the exchange knows the coin under the same denomination + * but with a different age commitment hash, and proves it with the + * denomination signature it holds for the coin. + */ +struct TALER_EXCHANGE_CoinAgeCommitmentConflict +{ + /** + * Public key of the coin. + */ + struct TALER_CoinSpendPublicKeyP coin_pub; + + /** + * Hash of the denomination of the coin (equals the one the + * client used). + */ + struct TALER_DenominationHashP h_denom_pub; + + /** + * Age commitment hash the exchange stored for the coin. + * Only valid if @e no_expected_age_commitment is false. + */ + struct TALER_AgeCommitmentHashP expected_age_commitment_hash; + + /** + * True if the exchange stored the coin without an age + * commitment hash. + */ + bool no_expected_age_commitment; + + /** + * Signature by the denomination over the coin and + * @e expected_age_commitment_hash. + */ + struct TALER_DenominationSignature prev_denom_sig; + + /** + * Human-readable description of the mismatch given by the + * exchange. Only valid during the callback. + */ + const char *conflict_detail; + + /** + * True if @e prev_denom_sig was verified against the denomination + * from our exchange keys. False if @e h_denom_pub is not among + * the denominations we know; the response is then accepted as + * unverifiable. + */ + bool verified; +}; + + +/** + * Details of a 409 response about a coin conflict. + */ +struct TALER_EXCHANGE_CoinConflict +{ + /** + * Which conflict it is: #TALER_EC_EXCHANGE_GENERIC_COIN_CONFLICTING_DENOMINATION_KEY + * or #TALER_EC_EXCHANGE_GENERIC_COIN_CONFLICTING_AGE_HASH, and the + * discriminator of @e details. Only meaningful if the HTTP + * response was a 409 with one of these error codes: the struct + * usually lives in a union with the details of other responses. + */ + enum TALER_ErrorCode ec; + + union + { + /** + * Valid if @e ec is + * #TALER_EC_EXCHANGE_GENERIC_COIN_CONFLICTING_DENOMINATION_KEY. + */ + struct TALER_EXCHANGE_CoinDenominationConflict denomination_conflict; + + /** + * Valid if @e ec is + * #TALER_EC_EXCHANGE_GENERIC_COIN_CONFLICTING_AGE_HASH. + */ + struct TALER_EXCHANGE_CoinAgeCommitmentConflict age_commitment_conflict; + } details; +}; + + +/** * Information about a coin to be deposited into a purse or reserve. */ struct TALER_EXCHANGE_PurseDeposit diff --git a/src/include/taler/exchange/post-batch-deposit.h b/src/include/taler/exchange/post-batch-deposit.h @@ -350,26 +350,14 @@ struct TALER_EXCHANGE_PostBatchDepositResponse struct TALER_DenominationHashP h_denom_pub; } insufficient_funds; - struct - { - /** - * The coin that had a conflict. - */ - struct TALER_CoinSpendPublicKeyP coin_pub; - - /** - * Hash of the denomination public key of the coin. - */ - struct TALER_DenominationHashP h_denom_pub; - } coin_conflicting_age_hash; - - struct - { - /** - * The coin that had a conflict. - */ - struct TALER_CoinSpendPublicKeyP coin_pub; - } coin_conflicting_denomination_key; + /** + * Details if the error code is + * #TALER_EC_EXCHANGE_GENERIC_COIN_CONFLICTING_DENOMINATION_KEY + * or #TALER_EC_EXCHANGE_GENERIC_COIN_CONFLICTING_AGE_HASH. + * The proof was checked by the library, see the @e verified + * fields. + */ + struct TALER_EXCHANGE_CoinConflict coin_conflict; } details; diff --git a/src/include/taler/exchange/post-melt.h b/src/include/taler/exchange/post-melt.h @@ -111,6 +111,16 @@ struct TALER_EXCHANGE_PostMeltResponse struct TALER_RefreshCommitmentP rc; } ok; + /** + * Details if the HTTP status is #MHD_HTTP_CONFLICT and the error + * code is #TALER_EC_EXCHANGE_GENERIC_COIN_CONFLICTING_DENOMINATION_KEY + * (@e denomination_conflict) or + * #TALER_EC_EXCHANGE_GENERIC_COIN_CONFLICTING_AGE_HASH + * (@e age_commitment_conflict). The proof was checked by the + * library, see the @e verified fields. + */ + struct TALER_EXCHANGE_CoinConflict conflict; + } details; }; diff --git a/src/include/taler/exchange/post-purses-PURSE_PUB-create.h b/src/include/taler/exchange/post-purses-PURSE_PUB-create.h @@ -200,6 +200,16 @@ struct TALER_EXCHANGE_PostPursesCreateResponse } ok; + /** + * Details if the HTTP status is #MHD_HTTP_CONFLICT and the error + * code is #TALER_EC_EXCHANGE_GENERIC_COIN_CONFLICTING_DENOMINATION_KEY + * (@e denomination_conflict) or + * #TALER_EC_EXCHANGE_GENERIC_COIN_CONFLICTING_AGE_HASH + * (@e age_commitment_conflict). The proof was checked by the + * library, see the @e verified fields. + */ + struct TALER_EXCHANGE_CoinConflict conflict; + } details; }; diff --git a/src/include/taler/exchange/post-purses-PURSE_PUB-deposit.h b/src/include/taler/exchange/post-purses-PURSE_PUB-deposit.h @@ -106,6 +106,17 @@ struct TALER_EXCHANGE_PostPursesDepositResponse struct TALER_ExchangePublicKeyP exchange_pub; } ok; + + /** + * Details if the HTTP status is #MHD_HTTP_CONFLICT and the error + * code is #TALER_EC_EXCHANGE_GENERIC_COIN_CONFLICTING_DENOMINATION_KEY + * (@e denomination_conflict) or + * #TALER_EC_EXCHANGE_GENERIC_COIN_CONFLICTING_AGE_HASH + * (@e age_commitment_conflict). The proof was checked by the + * library, see the @e verified fields. + */ + struct TALER_EXCHANGE_CoinConflict conflict; + } details; }; diff --git a/src/include/taler/exchange/post-recoup-refresh.h b/src/include/taler/exchange/post-recoup-refresh.h @@ -124,6 +124,17 @@ struct TALER_EXCHANGE_PostRecoupRefreshResponse */ struct TALER_ExchangePublicKeyP exchange_pub; } ok; + + /** + * Details if the HTTP status is #MHD_HTTP_CONFLICT and the error + * code is #TALER_EC_EXCHANGE_GENERIC_COIN_CONFLICTING_DENOMINATION_KEY + * (@e denomination_conflict) or + * #TALER_EC_EXCHANGE_GENERIC_COIN_CONFLICTING_AGE_HASH + * (@e age_commitment_conflict). The proof was checked by the + * library, see the @e verified fields. + */ + struct TALER_EXCHANGE_CoinConflict conflict; + } details; }; diff --git a/src/include/taler/exchange/post-recoup-withdraw.h b/src/include/taler/exchange/post-recoup-withdraw.h @@ -162,6 +162,17 @@ struct TALER_EXCHANGE_PostRecoupWithdrawResponse */ struct TALER_ExchangePublicKeyP exchange_pub; } ok; + + /** + * Details if the HTTP status is #MHD_HTTP_CONFLICT and the error + * code is #TALER_EC_EXCHANGE_GENERIC_COIN_CONFLICTING_DENOMINATION_KEY + * (@e denomination_conflict) or + * #TALER_EC_EXCHANGE_GENERIC_COIN_CONFLICTING_AGE_HASH + * (@e age_commitment_conflict). The proof was checked by the + * library, see the @e verified fields. + */ + struct TALER_EXCHANGE_CoinConflict conflict; + } details; }; diff --git a/src/include/taler/exchange/post-reserves-RESERVE_PUB-open.h b/src/include/taler/exchange/post-reserves-RESERVE_PUB-open.h @@ -119,6 +119,13 @@ struct TALER_EXCHANGE_PostReservesOpenResponse */ struct TALER_CoinSpendPublicKeyP coin_pub; + /** + * Details if the error code is one of the coin conflicts. + * The proof was checked by the library, see the @e verified + * fields. + */ + struct TALER_EXCHANGE_CoinConflict coin_conflict; + } conflict; /** diff --git a/src/json/json_helper.c b/src/json/json_helper.c @@ -884,6 +884,8 @@ parse_denom_pub (void *cls, GNUNET_free (bsign_pub); return GNUNET_SYSERR; } + GNUNET_CRYPTO_rsa_public_key_hash (bsign_pub->details.rsa_public_key, + &bsign_pub->pub_key_hash); denom_pub->bsign_pub_key = bsign_pub; return GNUNET_OK; } @@ -906,6 +908,9 @@ parse_denom_pub (void *cls, GNUNET_free (bsign_pub); return GNUNET_SYSERR; } + GNUNET_CRYPTO_hash (&bsign_pub->details.cs_public_key, + sizeof(bsign_pub->details.cs_public_key), + &bsign_pub->pub_key_hash); denom_pub->bsign_pub_key = bsign_pub; return GNUNET_OK; } @@ -1128,6 +1133,8 @@ parse_denom_pub_cipher (void *cls, GNUNET_free (bsign_pub); return GNUNET_SYSERR; } + GNUNET_CRYPTO_rsa_public_key_hash (bsign_pub->details.rsa_public_key, + &bsign_pub->pub_key_hash); denom_pub->bsign_pub_key = bsign_pub; return GNUNET_OK; } @@ -1150,6 +1157,9 @@ parse_denom_pub_cipher (void *cls, GNUNET_free (bsign_pub); return GNUNET_SYSERR; } + GNUNET_CRYPTO_hash (&bsign_pub->details.cs_public_key, + sizeof(bsign_pub->details.cs_public_key), + &bsign_pub->pub_key_hash); denom_pub->bsign_pub_key = bsign_pub; return GNUNET_OK; } diff --git a/src/lib/exchange_api_common.c b/src/lib/exchange_api_common.c @@ -20,6 +20,7 @@ * @author Christian Grothoff */ #include "taler/taler_json_lib.h" +#include <microhttpd.h> #include <gnunet/gnunet_curl_lib.h> #include "exchange_api_common.h" #include "exchange_api_handle.h" @@ -274,19 +275,139 @@ TALER_EXCHANGE_check_purse_econtract_conflict_ ( } -// FIXME: should be used... - #9422 +enum GNUNET_GenericReturnValue +TALER_EXCHANGE_parse_coin_denomination_conflict_ ( + const json_t *proof, + struct TALER_EXCHANGE_CoinDenominationConflict *cdc) +{ + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_fixed_auto ("coin_pub", + &cdc->coin_pub), + TALER_JSON_spec_denom_pub ("prev_denom_pub", + &cdc->prev_denom_pub), + TALER_JSON_spec_denom_sig ("prev_denom_sig", + &cdc->prev_denom_sig), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_fixed_auto ("prev_h_age_commitment", + &cdc->prev_h_age_commitment), + &cdc->no_prev_age_commitment), + GNUNET_JSON_spec_end () + }; + + memset (cdc, + 0, + sizeof (*cdc)); + if (GNUNET_OK != + GNUNET_JSON_parse (proof, + spec, + NULL, NULL)) + { + GNUNET_break_op (0); + return GNUNET_SYSERR; + } + TALER_denom_pub_hash (&cdc->prev_denom_pub, + &cdc->prev_h_denom_pub); + cdc->verified = false; + return GNUNET_OK; +} + + enum GNUNET_GenericReturnValue TALER_EXCHANGE_check_coin_denomination_conflict_ ( + const struct TALER_EXCHANGE_Keys *keys, + const struct TALER_DenominationHashP *h_denom_pub, + struct TALER_EXCHANGE_CoinDenominationConflict *cdc) +{ + const struct TALER_EXCHANGE_DenomPublicKey *dk; + struct TALER_CoinPublicInfo cpi = { + .coin_pub = cdc->coin_pub, + .denom_pub_hash = cdc->prev_h_denom_pub, + .h_age_commitment = cdc->prev_h_age_commitment, + .no_age_commitment = cdc->no_prev_age_commitment, + .denom_sig = cdc->prev_denom_sig + }; + + cdc->verified = false; + if (0 == + GNUNET_memcmp (h_denom_pub, + &cdc->prev_h_denom_pub)) + { + /* Must be a DIFFERENT denomination, not a conflict! */ + GNUNET_break_op (0); + return GNUNET_SYSERR; + } + dk = TALER_EXCHANGE_get_denomination_key_by_hash (keys, + &cdc->prev_h_denom_pub); + if (NULL == dk) + { + /* TODO[oec]: the exchange names a denomination that is not (or no + longer) in our /keys, for example one that expired. We cannot + verify the signature, but we also cannot tell that the proof is + wrong. We accept the response as unverifiable and leave the + decision to the application (see @e verified). */ + return GNUNET_NO; + } + /* the age commitment hash must be present exactly if the + denomination is age-restricted */ + if (cdc->no_prev_age_commitment != + (0 == dk->key.age_mask.bits)) + { + GNUNET_break_op (0); + return GNUNET_SYSERR; + } + if (0 != + TALER_denom_pub_cmp (&dk->key, + &cdc->prev_denom_pub)) + { + /* hash matches, key does not?! */ + GNUNET_break_op (0); + return GNUNET_SYSERR; + } + if (GNUNET_OK != + TALER_test_coin_valid (&cpi, + &cdc->prev_denom_pub)) + { + GNUNET_break_op (0); + return GNUNET_SYSERR; + } + cdc->verified = true; + return GNUNET_OK; +} + + +void +TALER_EXCHANGE_free_coin_denomination_conflict_ ( + struct TALER_EXCHANGE_CoinDenominationConflict *cdc) +{ + TALER_denom_pub_free (&cdc->prev_denom_pub); + TALER_denom_sig_free (&cdc->prev_denom_sig); +} + + +enum GNUNET_GenericReturnValue +TALER_EXCHANGE_parse_coin_age_commitment_conflict_ ( const json_t *proof, - const struct TALER_DenominationHashP *ch_denom_pub) + struct TALER_EXCHANGE_CoinAgeCommitmentConflict *cac) { - struct TALER_DenominationHashP h_denom_pub; struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_fixed_auto ("coin_pub", + &cac->coin_pub), GNUNET_JSON_spec_fixed_auto ("h_denom_pub", - &h_denom_pub), + &cac->h_denom_pub), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_fixed_auto ("expected_age_commitment_hash", + &cac->expected_age_commitment_hash), + &cac->no_expected_age_commitment), + GNUNET_JSON_spec_string ("conflict_detail", + &cac->conflict_detail), + TALER_JSON_spec_denom_sig ("prev_denom_sig", + &cac->prev_denom_sig), GNUNET_JSON_spec_end () }; + memset (cac, + 0, + sizeof (*cac)); if (GNUNET_OK != GNUNET_JSON_parse (proof, spec, @@ -295,18 +416,187 @@ TALER_EXCHANGE_check_coin_denomination_conflict_ ( GNUNET_break_op (0); return GNUNET_SYSERR; } - if (0 == - GNUNET_memcmp (ch_denom_pub, - &h_denom_pub)) + cac->verified = false; + return GNUNET_OK; +} + + +enum GNUNET_GenericReturnValue +TALER_EXCHANGE_check_coin_age_commitment_conflict_ ( + const struct TALER_EXCHANGE_Keys *keys, + const struct TALER_DenominationHashP *h_denom_pub, + const struct TALER_AgeCommitmentHashP *h_age_commitment, + struct TALER_EXCHANGE_CoinAgeCommitmentConflict *cac) +{ + const struct TALER_EXCHANGE_DenomPublicKey *dk; + struct TALER_CoinPublicInfo cpi = { + .coin_pub = cac->coin_pub, + .denom_pub_hash = cac->h_denom_pub, + .h_age_commitment = cac->expected_age_commitment_hash, + .no_age_commitment = cac->no_expected_age_commitment, + .denom_sig = cac->prev_denom_sig + }; + + cac->verified = false; + if (0 != + GNUNET_memcmp (h_denom_pub, + &cac->h_denom_pub)) { + /* an age conflict is about the SAME denomination */ GNUNET_break_op (0); + return GNUNET_SYSERR; + } + if (cac->no_expected_age_commitment == (NULL == h_age_commitment)) + { + /* both absent, or both present: then they must differ */ + if ( (cac->no_expected_age_commitment) || + (0 == + GNUNET_memcmp (h_age_commitment, + &cac->expected_age_commitment_hash)) ) + { + /* Must be a DIFFERENT age commitment, not a conflict! */ + GNUNET_break_op (0); + return GNUNET_SYSERR; + } + } + dk = TALER_EXCHANGE_get_denomination_key_by_hash (keys, + &cac->h_denom_pub); + if (NULL == dk) + { + /* TODO[oec]: as for the denomination conflict, the denomination is + not in our /keys, so the signature cannot be checked; accept the + response as unverifiable. */ return GNUNET_NO; } - /* indeed, proof with different denomination key provided */ + if (GNUNET_OK != + TALER_test_coin_valid (&cpi, + &dk->key)) + { + GNUNET_break_op (0); + return GNUNET_SYSERR; + } + cac->verified = true; return GNUNET_OK; } +void +TALER_EXCHANGE_free_coin_age_commitment_conflict_ ( + struct TALER_EXCHANGE_CoinAgeCommitmentConflict *cac) +{ + TALER_denom_sig_free (&cac->prev_denom_sig); + cac->conflict_detail = NULL; +} + + +enum GNUNET_GenericReturnValue +TALER_EXCHANGE_check_coin_conflict_ ( + const struct TALER_EXCHANGE_Keys *keys, + enum TALER_ErrorCode ec, + const json_t *proof, + TALER_EXCHANGE_CoinLookupCallback_ lookup, + void *lookup_cls, + struct TALER_EXCHANGE_CoinConflict *cc) +{ + const struct TALER_DenominationHashP *h_denom_pub = NULL; + const struct TALER_AgeCommitmentHashP *h_age_commitment = NULL; + + cc->ec = TALER_EC_NONE; + switch (ec) + { + case TALER_EC_EXCHANGE_GENERIC_COIN_CONFLICTING_DENOMINATION_KEY: + { + struct TALER_EXCHANGE_CoinDenominationConflict *cdc + = &cc->details.denomination_conflict; + + if (GNUNET_OK != + TALER_EXCHANGE_parse_coin_denomination_conflict_ (proof, + cdc)) + { + GNUNET_break_op (0); + return GNUNET_SYSERR; + } + if ( (GNUNET_OK != + lookup (lookup_cls, + &cdc->coin_pub, + &h_denom_pub, + &h_age_commitment)) || + (GNUNET_SYSERR == + TALER_EXCHANGE_check_coin_denomination_conflict_ (keys, + h_denom_pub, + cdc)) ) + { + GNUNET_break_op (0); + TALER_EXCHANGE_free_coin_denomination_conflict_ (cdc); + return GNUNET_SYSERR; + } + cc->ec = ec; + return GNUNET_OK; + } + case TALER_EC_EXCHANGE_GENERIC_COIN_CONFLICTING_AGE_HASH: + { + struct TALER_EXCHANGE_CoinAgeCommitmentConflict *cac + = &cc->details.age_commitment_conflict; + + if (GNUNET_OK != + TALER_EXCHANGE_parse_coin_age_commitment_conflict_ (proof, + cac)) + { + GNUNET_break_op (0); + return GNUNET_SYSERR; + } + if ( (GNUNET_OK != + lookup (lookup_cls, + &cac->coin_pub, + &h_denom_pub, + &h_age_commitment)) || + (GNUNET_SYSERR == + TALER_EXCHANGE_check_coin_age_commitment_conflict_ ( + keys, + h_denom_pub, + h_age_commitment, + cac)) ) + { + GNUNET_break_op (0); + TALER_EXCHANGE_free_coin_age_commitment_conflict_ (cac); + return GNUNET_SYSERR; + } + cc->ec = ec; + return GNUNET_OK; + } + default: + GNUNET_break (0); + return GNUNET_SYSERR; + } +} + + +void +TALER_EXCHANGE_free_coin_conflict_ ( + const struct TALER_EXCHANGE_HttpResponse *hr, + struct TALER_EXCHANGE_CoinConflict *cc) +{ + if (MHD_HTTP_CONFLICT != hr->http_status) + return; + if (hr->ec != cc->ec) + return; + switch (cc->ec) + { + case TALER_EC_EXCHANGE_GENERIC_COIN_CONFLICTING_DENOMINATION_KEY: + TALER_EXCHANGE_free_coin_denomination_conflict_ ( + &cc->details.denomination_conflict); + break; + case TALER_EC_EXCHANGE_GENERIC_COIN_CONFLICTING_AGE_HASH: + TALER_EXCHANGE_free_coin_age_commitment_conflict_ ( + &cc->details.age_commitment_conflict); + break; + default: + break; + } + cc->ec = TALER_EC_NONE; +} + + enum GNUNET_GenericReturnValue TALER_EXCHANGE_get_min_denomination_ ( const struct TALER_EXCHANGE_Keys *keys, @@ -769,6 +1059,7 @@ TALER_EXCHANGE_recoup_coin_data_ ( const struct TALER_BlindingMasterSeedP *blinding_seed, bool for_melt, struct TALER_CoinSpendPublicKeyP recouped_pubs[static num_coins], + struct TALER_EXCHANGE_RecoupedCoinInfo_ recouped_infos[static num_coins], size_t *num_recouped) { json_t *arr = json_array (); @@ -893,6 +1184,10 @@ TALER_EXCHANGE_recoup_coin_data_ ( GNUNET_JSON_pack_data_auto ("coin_sig", &coin_sig)); recouped_pubs[*num_recouped] = coin_pub; + recouped_infos[*num_recouped].h_denom_pub = c->pk->h_key; + recouped_infos[*num_recouped].have_age = (NULL != c->h_age_commitment); + if (NULL != c->h_age_commitment) + recouped_infos[*num_recouped].h_age_commitment = *c->h_age_commitment; (*num_recouped)++; } GNUNET_assert (0 == diff --git a/src/lib/exchange_api_common.h b/src/lib/exchange_api_common.h @@ -109,43 +109,174 @@ TALER_EXCHANGE_check_purse_econtract_conflict_ ( /** - * Check proof of a coin spend value conflict. + * Parse a #TALER_EC_EXCHANGE_GENERIC_COIN_CONFLICTING_DENOMINATION_KEY + * response. On success, @a cdc must be released with + * #TALER_EXCHANGE_free_coin_denomination_conflict_(). * - * @param keys exchange /keys structure - * @param proof the proof to check - * @param[out] coin_pub set to the public key of the - * coin that is claimed to have an insufficient - * balance - * @param[out] remaining set to the remaining balance - * of the coin as provided by the proof - * @return #GNUNET_OK if the @a proof is OK for @a purse_pub demonstrating that @a coin_pub has only @a remaining balance. + * @param proof the response body to parse + * @param[out] cdc set to the parsed details, @e verified is left false + * @return #GNUNET_OK on success, #GNUNET_SYSERR if @a proof is malformed */ enum GNUNET_GenericReturnValue -TALER_EXCHANGE_check_coin_amount_conflict_ ( - const struct TALER_EXCHANGE_Keys *keys, +TALER_EXCHANGE_parse_coin_denomination_conflict_ ( const json_t *proof, - struct TALER_CoinSpendPublicKeyP *coin_pub, - struct TALER_Amount *remaining); + struct TALER_EXCHANGE_CoinDenominationConflict *cdc); /** - * Verify that @a proof contains a coin history that demonstrates that @a - * coin_pub was previously used with a denomination key that is different from - * @a ch_denom_pub. Note that the coin history MUST have been checked before - * using #TALER_EXCHANGE_check_coin_amount_conflict_(). + * Check that the parsed @a cdc proves a denomination conflict for a coin + * the client used with denomination @a h_denom_pub: the denomination + * named by the exchange must differ from @a h_denom_pub, and the + * signature it provides must be valid for the coin under that + * denomination. * - * @param proof a proof to check - * @param ch_denom_pub hash of the conflicting denomination - * @return #GNUNET_OK if @a ch_denom_pub differs from the - * denomination hash given by the history of the coin, - * #GNUNET_NO if the proof is for the same @a ch_denom_pub - * and thus invalid - * #GNUNET_SYSERR if the proof failed to parse (also invalid) + * @param keys exchange /keys structure, used to look up the + * denomination named in @a cdc + * @param h_denom_pub hash of the denomination the client used for the coin + * @param[in,out] cdc the parsed conflict; @e verified is set + * @return #GNUNET_OK if the proof is valid and was verified, + * #GNUNET_NO if the proof is consistent but names a denomination + * that is not in @a keys, so the signature could not be checked + * (the proof is accepted as unverifiable), + * #GNUNET_SYSERR if the proof is invalid */ enum GNUNET_GenericReturnValue TALER_EXCHANGE_check_coin_denomination_conflict_ ( + const struct TALER_EXCHANGE_Keys *keys, + const struct TALER_DenominationHashP *h_denom_pub, + struct TALER_EXCHANGE_CoinDenominationConflict *cdc); + + +/** + * Release the memory held by @a cdc. + * + * @param[in] cdc parsed conflict to release + */ +void +TALER_EXCHANGE_free_coin_denomination_conflict_ ( + struct TALER_EXCHANGE_CoinDenominationConflict *cdc); + + +/** + * Parse a #TALER_EC_EXCHANGE_GENERIC_COIN_CONFLICTING_AGE_HASH + * response. On success, @a cac must be released with + * #TALER_EXCHANGE_free_coin_age_commitment_conflict_(). + * + * @param proof the response body to parse + * @param[out] cac set to the parsed details, @e verified is left false + * @return #GNUNET_OK on success, #GNUNET_SYSERR if @a proof is malformed + */ +enum GNUNET_GenericReturnValue +TALER_EXCHANGE_parse_coin_age_commitment_conflict_ ( + const json_t *proof, + struct TALER_EXCHANGE_CoinAgeCommitmentConflict *cac); + + +/** + * Check that the parsed @a cac proves an age commitment conflict for a + * coin the client used with denomination @a h_denom_pub and age + * commitment hash @a h_age_commitment: the denomination must match, + * the stored age commitment hash must differ, and the signature the + * exchange provides must be valid for the coin with the stored hash. + * + * @param keys exchange /keys structure, used to look up the denomination + * @param h_denom_pub hash of the denomination the client used for the coin + * @param h_age_commitment age commitment hash the client used for the + * coin, NULL if none + * @param[in,out] cac the parsed conflict; @e verified is set + * @return #GNUNET_OK if the proof is valid and was verified, + * #GNUNET_NO if the proof is consistent but the denomination is + * not in @a keys, so the signature could not be checked + * (the proof is accepted as unverifiable), + * #GNUNET_SYSERR if the proof is invalid + */ +enum GNUNET_GenericReturnValue +TALER_EXCHANGE_check_coin_age_commitment_conflict_ ( + const struct TALER_EXCHANGE_Keys *keys, + const struct TALER_DenominationHashP *h_denom_pub, + const struct TALER_AgeCommitmentHashP *h_age_commitment, + struct TALER_EXCHANGE_CoinAgeCommitmentConflict *cac); + + +/** + * Release the memory held by @a cac. + * + * @param[in] cac parsed conflict to release + */ +void +TALER_EXCHANGE_free_coin_age_commitment_conflict_ ( + struct TALER_EXCHANGE_CoinAgeCommitmentConflict *cac); + + +/** + * Function called by #TALER_EXCHANGE_check_coin_conflict_() to find + * out how the client used the coin @a coin_pub in the request. + * + * @param cls closure + * @param coin_pub public key of the coin named in the conflict reply + * @param[out] h_denom_pub set to the hash of the denomination the + * client used for the coin + * @param[out] h_age_commitment set to the age commitment hash the + * client used for the coin, or NULL if none + * @return #GNUNET_OK if the coin was found and the outputs are set, + * #GNUNET_NO if the request did not contain @a coin_pub + */ +typedef enum GNUNET_GenericReturnValue +(*TALER_EXCHANGE_CoinLookupCallback_)( + void *cls, + const struct TALER_CoinSpendPublicKeyP *coin_pub, + const struct TALER_DenominationHashP **h_denom_pub, + const struct TALER_AgeCommitmentHashP **h_age_commitment); + + +/** + * Handle a 409 reply with error code @a ec, which must be + * #TALER_EC_EXCHANGE_GENERIC_COIN_CONFLICTING_DENOMINATION_KEY or + * #TALER_EC_EXCHANGE_GENERIC_COIN_CONFLICTING_AGE_HASH: parse @a proof + * into @a cc, find the coin it names via @a lookup, and check the + * proof against what the client used for that coin (see + * #TALER_EXCHANGE_check_coin_denomination_conflict_() and + * #TALER_EXCHANGE_check_coin_age_commitment_conflict_()). + * + * On success, @a cc must be released with + * #TALER_EXCHANGE_free_coin_conflict_() once the application was + * informed. On failure, @a cc is already released and its + * @e ec is #TALER_EC_NONE. + * + * @param keys exchange /keys structure + * @param ec error code of the reply + * @param proof body of the reply + * @param lookup function to find the coin in the request + * @param lookup_cls closure for @a lookup + * @param[out] cc set to the parsed and checked reply, with @e ec set to @a ec + * @return #GNUNET_OK if the proof is valid (verified, or accepted as + * unverifiable), #GNUNET_SYSERR if the reply is malformed or + * the proof is invalid + */ +enum GNUNET_GenericReturnValue +TALER_EXCHANGE_check_coin_conflict_ ( + const struct TALER_EXCHANGE_Keys *keys, + enum TALER_ErrorCode ec, const json_t *proof, - const struct TALER_DenominationHashP *ch_denom_pub); + TALER_EXCHANGE_CoinLookupCallback_ lookup, + void *lookup_cls, + struct TALER_EXCHANGE_CoinConflict *cc); + + +/** + * Release the memory held by @a cc, if @a hr says that the reply was + * a coin conflict and @a cc was filled by + * #TALER_EXCHANGE_check_coin_conflict_(). @a cc usually lives in a + * union with the details of other replies, so its own @e ec must not + * be trusted before @a hr was consulted. + * + * @param hr HTTP response the details belong to + * @param[in] cc conflict details to release; @e ec is reset + */ +void +TALER_EXCHANGE_free_coin_conflict_ ( + const struct TALER_EXCHANGE_HttpResponse *hr, + struct TALER_EXCHANGE_CoinConflict *cc); /** @@ -181,6 +312,29 @@ TALER_EXCHANGE_verify_deposit_signature_ ( /** + * Denomination and age commitment of a coin disclosed for recoup, + * kept to check a conflict reply against. + */ +struct TALER_EXCHANGE_RecoupedCoinInfo_ +{ + /** + * Hash of the denomination of the coin. + */ + struct TALER_DenominationHashP h_denom_pub; + + /** + * Age commitment hash of the coin, if @e have_age. + */ + struct TALER_AgeCommitmentHashP h_age_commitment; + + /** + * True if the coin has an age commitment. + */ + bool have_age; +}; + + +/** * Build the ``coin_data`` array of a recoup request: the hash of the * blinded envelope for coins that are not to be recouped, the disclosed * coin with its recoup signature for the others. @@ -191,6 +345,9 @@ TALER_EXCHANGE_verify_deposit_signature_ ( * are no CS coins * @param for_melt true if the operation was a melt (affects the * nonce derivation and the signature purpose) + * @param[out] recouped_infos array of @a num_coins entries, set to the + * denomination and age commitment of the recouped coins, in the + * order of @a recouped_pubs * @param[out] recouped_pubs array of @a num_coins entries, set to the * public keys of the recouped coins, in order * @param[out] num_recouped set to the number of recouped coins @@ -204,6 +361,7 @@ TALER_EXCHANGE_recoup_coin_data_ ( const struct TALER_BlindingMasterSeedP *blinding_seed, bool for_melt, struct TALER_CoinSpendPublicKeyP recouped_pubs[static num_coins], + struct TALER_EXCHANGE_RecoupedCoinInfo_ recouped_infos[static num_coins], size_t *num_recouped); diff --git a/src/lib/exchange_api_get-coins-COIN_PUB-history.c b/src/lib/exchange_api_get-coins-COIN_PUB-history.c @@ -1266,56 +1266,4 @@ TALER_EXCHANGE_check_coin_signature_conflict ( } -#if FIXME_IMPLEMENT /* #9422 */ -/** - * FIXME-Oec-#9422: we need some specific routines that show - * that certain coin operations are indeed in conflict, - * for example that the coin is of a different denomination - * or different age restrictions. - * This relates to unimplemented error handling for - * coins in the exchange! - * - * Check that the provided @a proof indeeds indicates - * a conflict for @a coin_pub. - * - * @param keys exchange keys - * @param proof provided conflict proof - * @param dk denomination of @a coin_pub that the client - * used - * @param coin_pub public key of the coin - * @param required balance required on the coin for the operation - * @return #GNUNET_OK if @a proof holds - */ -// FIXME-#9422: should be properly defined and implemented! -enum GNUNET_GenericReturnValue -TALER_EXCHANGE_check_coin_conflict_ ( - const struct TALER_EXCHANGE_Keys *keys, - const json_t *proof, - const struct TALER_EXCHANGE_DenomPublicKey *dk, - const struct TALER_CoinSpendPublicKeyP *coin_pub, - const struct TALER_Amount *required) -{ - enum TALER_ErrorCode ec; - - ec = TALER_JSON_get_error_code (proof); - switch (ec) - { - case TALER_EC_EXCHANGE_GENERIC_INSUFFICIENT_FUNDS: - /* Nothing to check anymore here, proof needs to be - checked in the GET /coins/$COIN_PUB handler */ - break; - case TALER_EC_EXCHANGE_GENERIC_COIN_CONFLICTING_DENOMINATION_KEY: - // FIXME-#9422: write check! - break; - default: - GNUNET_break_op (0); - return GNUNET_SYSERR; - } - return GNUNET_OK; -} - - -#endif - - /* end of exchange_api_get-coins-COIN_PUB-history.c */ diff --git a/src/lib/exchange_api_post-batch-deposit.c b/src/lib/exchange_api_post-batch-deposit.c @@ -219,6 +219,9 @@ finish_dh (struct TALER_EXCHANGE_PostBatchDepositHandle *dh) { dh->cb (dh->cb_cls, &dh->dr); + TALER_EXCHANGE_free_coin_conflict_ (&dh->dr.hr, + &dh->dr.details.conflict.details. + coin_conflict); TALER_EXCHANGE_post_batch_deposit_cancel (dh); } @@ -340,6 +343,40 @@ auditor_cb (void *cls, /** + * Find the deposited coin @a coin_pub and how we used it. + * + * @param cls a `struct TALER_EXCHANGE_PostBatchDepositHandle *` + * @param coin_pub public key of the coin named in the conflict reply + * @param[out] h_denom_pub set to the hash of the denomination we used + * @param[out] h_age_commitment set to the age commitment hash we used, or NULL + * @return #GNUNET_OK if found, #GNUNET_NO if the coin is not ours + */ +static enum GNUNET_GenericReturnValue +deposit_coin_lookup (void *cls, + const struct TALER_CoinSpendPublicKeyP *coin_pub, + const struct TALER_DenominationHashP **h_denom_pub, + const struct TALER_AgeCommitmentHashP **h_age_commitment) +{ + struct TALER_EXCHANGE_PostBatchDepositHandle *dh = cls; + + for (unsigned int i = 0; i<dh->num_cdds; i++) + { + const struct TALER_EXCHANGE_CoinDepositDetail *cdd = &dh->cdds[i]; + + if (0 != GNUNET_memcmp (coin_pub, + &cdd->coin_pub)) + continue; + *h_denom_pub = &cdd->h_denom_pub; + *h_age_commitment = GNUNET_is_zero (&cdd->h_age_commitment) + ? NULL + : &cdd->h_age_commitment; + return GNUNET_OK; + } + return GNUNET_NO; +} + + +/** * Function called when we're done processing the * HTTP /batch-deposit request. * @@ -505,52 +542,20 @@ handle_deposit_finished (void *cls, } } break; - case TALER_EC_EXCHANGE_GENERIC_COIN_CONFLICTING_AGE_HASH: - { - struct GNUNET_JSON_Specification spec[] = { - GNUNET_JSON_spec_fixed_auto ( - "coin_pub", - &dr->details.conflict.details - .coin_conflicting_age_hash.coin_pub), - GNUNET_JSON_spec_fixed_auto ( - "h_denom_pub", - &dr->details.conflict.details - .coin_conflicting_age_hash.h_denom_pub), - GNUNET_JSON_spec_end () - }; - - if (GNUNET_OK != - GNUNET_JSON_parse (j, - spec, - NULL, NULL)) - { - GNUNET_break_op (0); - dr->hr.http_status = 0; - dr->hr.ec = TALER_EC_GENERIC_REPLY_MALFORMED; - break; - } - } - break; case TALER_EC_EXCHANGE_GENERIC_COIN_CONFLICTING_DENOMINATION_KEY: + case TALER_EC_EXCHANGE_GENERIC_COIN_CONFLICTING_AGE_HASH: + if (GNUNET_OK != + TALER_EXCHANGE_check_coin_conflict_ (dh->keys, + dr->hr.ec, + j, + &deposit_coin_lookup, + dh, + &dr->details.conflict.details. + coin_conflict)) { - struct GNUNET_JSON_Specification spec[] = { - GNUNET_JSON_spec_fixed_auto ( - "coin_pub", - &dr->details.conflict.details - .coin_conflicting_denomination_key.coin_pub), - GNUNET_JSON_spec_end () - }; - - if (GNUNET_OK != - GNUNET_JSON_parse (j, - spec, - NULL, NULL)) - { - GNUNET_break_op (0); - dr->hr.http_status = 0; - dr->hr.ec = TALER_EC_GENERIC_REPLY_MALFORMED; - break; - } + GNUNET_break_op (0); + dr->hr.http_status = 0; + dr->hr.ec = TALER_EC_GENERIC_REPLY_MALFORMED; } break; case TALER_EC_EXCHANGE_DEPOSIT_CONFLICTING_CONTRACT: diff --git a/src/lib/exchange_api_post-melt.c b/src/lib/exchange_api_post-melt.c @@ -44,6 +44,12 @@ struct TALER_EXCHANGE_PostMeltHandle struct TALER_EXCHANGE_Keys *keys; /** + * Hash of the denomination of the melted coin, filled by + * #melt_coin_lookup() when checking a conflict reply. + */ + struct TALER_DenominationHashP h_melt_denom_pub; + + /** * The url for this request. */ char *url; @@ -204,6 +210,39 @@ verify_melt_signature_ok (struct TALER_EXCHANGE_PostMeltHandle *mh, /** + * Check that @a coin_pub is the melted coin and report how we used it. + * + * @param cls a `struct TALER_EXCHANGE_PostMeltHandle *` + * @param coin_pub public key of the coin named in the conflict reply + * @param[out] h_denom_pub set to the hash of the denomination we used + * @param[out] h_age_commitment set to the age commitment hash we used, or NULL + * @return #GNUNET_OK if found, #GNUNET_NO if the coin is not ours + */ +static enum GNUNET_GenericReturnValue +melt_coin_lookup (void *cls, + const struct TALER_CoinSpendPublicKeyP *coin_pub, + const struct TALER_DenominationHashP **h_denom_pub, + const struct TALER_AgeCommitmentHashP **h_age_commitment) +{ + struct TALER_EXCHANGE_PostMeltHandle *mh = cls; + + struct TALER_CoinSpendPublicKeyP my_pub; + + GNUNET_CRYPTO_eddsa_key_get_public ( + &mh->md.melted_coin.coin_priv.eddsa_priv, + &my_pub.eddsa_pub); + if (0 != GNUNET_memcmp (coin_pub, + &my_pub)) + return GNUNET_NO; + TALER_denom_pub_hash (&mh->md.melted_coin.pub_key, + &mh->h_melt_denom_pub); + *h_denom_pub = &mh->h_melt_denom_pub; + *h_age_commitment = mh->md.melted_coin.h_age_commitment; + return GNUNET_OK; +} + + +/** * Function called when we're done processing the * HTTP /melt request. * @@ -260,6 +299,28 @@ handle_melt_finished (void *cls, case MHD_HTTP_CONFLICT: mr.hr.ec = TALER_JSON_get_error_code (j); mr.hr.hint = TALER_JSON_get_error_hint (j); + switch (mr.hr.ec) + { + case TALER_EC_EXCHANGE_GENERIC_COIN_CONFLICTING_DENOMINATION_KEY: + case TALER_EC_EXCHANGE_GENERIC_COIN_CONFLICTING_AGE_HASH: + if (GNUNET_OK != + TALER_EXCHANGE_check_coin_conflict_ (mh->keys, + mr.hr.ec, + j, + &melt_coin_lookup, + mh, + &mr.details.conflict)) + { + GNUNET_break_op (0); + mr.hr.http_status = 0; + mr.hr.ec = TALER_EC_GENERIC_REPLY_MALFORMED; + } + break; + default: + /* insufficient funds: proof is the coin history, checked by + the application via GET /coins/$COIN_PUB/history */ + break; + } break; case MHD_HTTP_FORBIDDEN: /* Nothing really to verify, exchange says one of the signatures is @@ -294,6 +355,8 @@ handle_melt_finished (void *cls, if (NULL != mh->melt_cb) mh->melt_cb (mh->melt_cb_cls, &mr); + TALER_EXCHANGE_free_coin_conflict_ (&mr.hr, + &mr.details.conflict); TALER_EXCHANGE_post_melt_cancel (mh); } diff --git a/src/lib/exchange_api_post-purses-PURSE_PUB-create.c b/src/lib/exchange_api_post-purses-PURSE_PUB-create.c @@ -211,6 +211,41 @@ struct TALER_EXCHANGE_PostPursesCreateHandle /** + * Find the deposited coin @a coin_pub and how we used it. + * + * @param cls a `struct TALER_EXCHANGE_PostPursesCreateHandle *` + * @param coin_pub public key of the coin named in the conflict reply + * @param[out] h_denom_pub set to the hash of the denomination we used + * @param[out] h_age_commitment set to the age commitment hash we used, or NULL + * @return #GNUNET_OK if found, #GNUNET_NO if the coin is not ours + */ +static enum GNUNET_GenericReturnValue +purse_create_coin_lookup (void *cls, + const struct TALER_CoinSpendPublicKeyP *coin_pub, + const struct TALER_DenominationHashP **h_denom_pub, + const struct TALER_AgeCommitmentHashP ** + h_age_commitment) +{ + struct TALER_EXCHANGE_PostPursesCreateHandle *pch = cls; + + for (unsigned int i = 0; i<pch->num_deposits; i++) + { + const struct Deposit *deposit = &pch->deposits[i]; + + if (0 != GNUNET_memcmp (coin_pub, + &deposit->coin_pub)) + continue; + *h_denom_pub = &deposit->h_denom_pub; + *h_age_commitment = deposit->have_age + ? &deposit->ahac + : NULL; + return GNUNET_OK; + } + return GNUNET_NO; +} + + +/** * Function called when we're done processing the * HTTP /purses/$PID/create request. * @@ -331,7 +366,19 @@ handle_purse_create_deposit_finished (void *cls, checked in the GET /coins/$COIN_PUB handler */ break; case TALER_EC_EXCHANGE_GENERIC_COIN_CONFLICTING_DENOMINATION_KEY: - // FIXME #7267: write check (add to exchange_api_common!) */ + case TALER_EC_EXCHANGE_GENERIC_COIN_CONFLICTING_AGE_HASH: + if (GNUNET_OK != + TALER_EXCHANGE_check_coin_conflict_ (pch->keys, + dr.hr.ec, + j, + &purse_create_coin_lookup, + pch, + &dr.details.conflict)) + { + GNUNET_break_op (0); + dr.hr.http_status = 0; + dr.hr.ec = TALER_EC_GENERIC_REPLY_MALFORMED; + } break; case TALER_EC_EXCHANGE_PURSE_DEPOSIT_CONFLICTING_META_DATA: { @@ -452,6 +499,8 @@ handle_purse_create_deposit_finished (void *cls, &dr); pch->cb = NULL; } + TALER_EXCHANGE_free_coin_conflict_ (&dr.hr, + &dr.details.conflict); TALER_EXCHANGE_post_purses_create_cancel (pch); } diff --git a/src/lib/exchange_api_post-purses-PURSE_PUB-deposit.c b/src/lib/exchange_api_post-purses-PURSE_PUB-deposit.c @@ -134,6 +134,41 @@ struct TALER_EXCHANGE_PostPursesDepositHandle /** + * Find the deposited coin @a coin_pub and how we used it. + * + * @param cls a `struct TALER_EXCHANGE_PostPursesDepositHandle *` + * @param coin_pub public key of the coin named in the conflict reply + * @param[out] h_denom_pub set to the hash of the denomination we used + * @param[out] h_age_commitment set to the age commitment hash we used, or NULL + * @return #GNUNET_OK if found, #GNUNET_NO if the coin is not ours + */ +static enum GNUNET_GenericReturnValue +purse_deposit_coin_lookup (void *cls, + const struct TALER_CoinSpendPublicKeyP *coin_pub, + const struct TALER_DenominationHashP **h_denom_pub, + const struct TALER_AgeCommitmentHashP ** + h_age_commitment) +{ + struct TALER_EXCHANGE_PostPursesDepositHandle *pch = cls; + + for (unsigned int i = 0; i<pch->num_deposits; i++) + { + const struct Coin *coin = &pch->coins[i]; + + if (0 != GNUNET_memcmp (coin_pub, + &coin->coin_pub)) + continue; + *h_denom_pub = &coin->h_denom_pub; + *h_age_commitment = GNUNET_is_zero (&coin->ahac) + ? NULL + : &coin->ahac; + return GNUNET_OK; + } + return GNUNET_NO; +} + + +/** * Function called when we're done processing the * HTTP /purses/$PID/deposit request. * @@ -307,6 +342,19 @@ handle_purse_deposit_finished (void *cls, checked in the GET /coins/$COIN_PUB handler */ break; case TALER_EC_EXCHANGE_GENERIC_COIN_CONFLICTING_DENOMINATION_KEY: + case TALER_EC_EXCHANGE_GENERIC_COIN_CONFLICTING_AGE_HASH: + if (GNUNET_OK != + TALER_EXCHANGE_check_coin_conflict_ (pch->keys, + dr.hr.ec, + j, + &purse_deposit_coin_lookup, + pch, + &dr.details.conflict)) + { + GNUNET_break_op (0); + dr.hr.http_status = 0; + dr.hr.ec = TALER_EC_GENERIC_REPLY_MALFORMED; + } break; default: GNUNET_break_op (0); @@ -343,6 +391,8 @@ handle_purse_deposit_finished (void *cls, dr.hr.hint = TALER_ErrorCode_get_hint (dr.hr.ec); pch->cb (pch->cb_cls, &dr); + TALER_EXCHANGE_free_coin_conflict_ (&dr.hr, + &dr.details.conflict); TALER_EXCHANGE_post_purses_deposit_cancel (pch); } diff --git a/src/lib/exchange_api_post-recoup-refresh.c b/src/lib/exchange_api_post-recoup-refresh.c @@ -93,6 +93,12 @@ struct TALER_EXCHANGE_PostRecoupRefreshHandle struct TALER_CoinSpendPublicKeyP *recouped_pubs; /** + * Denomination and age commitment of the recouped coins, + * parallel to @e recouped_pubs. + */ + struct TALER_EXCHANGE_RecoupedCoinInfo_ *recouped_infos; + + /** * Number of entries in @e recouped_pubs. */ size_t num_recouped; @@ -204,6 +210,38 @@ process_ok_response ( /** + * Find the recouped coin @a coin_pub and how we used it. + * + * @param cls a `struct TALER_EXCHANGE_PostRecoupRefreshHandle *` + * @param coin_pub public key of the coin named in the conflict reply + * @param[out] h_denom_pub set to the hash of the denomination we used + * @param[out] h_age_commitment set to the age commitment hash we used, or NULL + * @return #GNUNET_OK if found, #GNUNET_NO if the coin is not ours + */ +static enum GNUNET_GenericReturnValue +recoup_coin_lookup (void *cls, + const struct TALER_CoinSpendPublicKeyP *coin_pub, + const struct TALER_DenominationHashP **h_denom_pub, + const struct TALER_AgeCommitmentHashP **h_age_commitment) +{ + struct TALER_EXCHANGE_PostRecoupRefreshHandle *prrh = cls; + + for (size_t i = 0; i < prrh->num_recouped; i++) + { + if (0 != GNUNET_memcmp (coin_pub, + &prrh->recouped_pubs[i])) + continue; + *h_denom_pub = &prrh->recouped_infos[i].h_denom_pub; + *h_age_commitment = prrh->recouped_infos[i].have_age + ? &prrh->recouped_infos[i].h_age_commitment + : NULL; + return GNUNET_OK; + } + return GNUNET_NO; +} + + +/** * Function called when we're done processing the * HTTP /recoup-refresh request. * @@ -261,10 +299,30 @@ handle_recoup_refresh_finished (void *cls, rr.hr.hint = TALER_JSON_get_error_hint (j); break; case MHD_HTTP_CONFLICT: - /* Commitment mismatch, denomination mismatch or a coin without - residual value; pass the JSON reply to the application */ rr.hr.ec = TALER_JSON_get_error_code (j); rr.hr.hint = TALER_JSON_get_error_hint (j); + switch (rr.hr.ec) + { + case TALER_EC_EXCHANGE_GENERIC_COIN_CONFLICTING_DENOMINATION_KEY: + case TALER_EC_EXCHANGE_GENERIC_COIN_CONFLICTING_AGE_HASH: + if (GNUNET_OK != + TALER_EXCHANGE_check_coin_conflict_ (prrh->keys, + rr.hr.ec, + j, + &recoup_coin_lookup, + prrh, + &rr.details.conflict)) + { + GNUNET_break_op (0); + rr.hr.http_status = 0; + rr.hr.ec = TALER_EC_GENERIC_REPLY_MALFORMED; + } + break; + default: + /* Commitment mismatch, denomination mismatch or a coin without + residual value; pass the JSON reply to the application */ + break; + } break; case MHD_HTTP_GONE: /* Denomination not eligible for recoup (not revoked, or expired) */ @@ -301,6 +359,8 @@ handle_recoup_refresh_finished (void *cls, } prrh->cb (prrh->cb_cls, &rr); + TALER_EXCHANGE_free_coin_conflict_ (&rr.hr, + &rr.details.conflict); TALER_EXCHANGE_post_recoup_refresh_cancel (prrh); } @@ -322,16 +382,21 @@ TALER_EXCHANGE_post_recoup_refresh_create ( prrh = GNUNET_new (struct TALER_EXCHANGE_PostRecoupRefreshHandle); prrh->recouped_pubs = GNUNET_new_array (num_coins, struct TALER_CoinSpendPublicKeyP); + prrh->recouped_infos = GNUNET_new_array ( + num_coins, + struct TALER_EXCHANGE_RecoupedCoinInfo_); coin_data = TALER_EXCHANGE_recoup_coin_data_ (num_coins, coins, blinding_seed, true, /* for melt */ prrh->recouped_pubs, + prrh->recouped_infos, &prrh->num_recouped); if (NULL == coin_data) { GNUNET_break (0); GNUNET_free (prrh->recouped_pubs); + GNUNET_free (prrh->recouped_infos); GNUNET_free (prrh); return NULL; } @@ -409,6 +474,7 @@ TALER_EXCHANGE_post_recoup_refresh_cancel ( GNUNET_free (prrh->url); GNUNET_free (prrh->base_url); GNUNET_free (prrh->recouped_pubs); + GNUNET_free (prrh->recouped_infos); json_decref (prrh->body); TALER_EXCHANGE_keys_decref (prrh->keys); GNUNET_free (prrh); diff --git a/src/lib/exchange_api_post-recoup-withdraw.c b/src/lib/exchange_api_post-recoup-withdraw.c @@ -93,6 +93,12 @@ struct TALER_EXCHANGE_PostRecoupWithdrawHandle struct TALER_CoinSpendPublicKeyP *recouped_pubs; /** + * Denomination and age commitment of the recouped coins, + * parallel to @e recouped_pubs. + */ + struct TALER_EXCHANGE_RecoupedCoinInfo_ *recouped_infos; + + /** * Number of entries in @e recouped_pubs. */ size_t num_recouped; @@ -204,6 +210,38 @@ process_ok_response ( /** + * Find the recouped coin @a coin_pub and how we used it. + * + * @param cls a `struct TALER_EXCHANGE_PostRecoupWithdrawHandle *` + * @param coin_pub public key of the coin named in the conflict reply + * @param[out] h_denom_pub set to the hash of the denomination we used + * @param[out] h_age_commitment set to the age commitment hash we used, or NULL + * @return #GNUNET_OK if found, #GNUNET_NO if the coin is not ours + */ +static enum GNUNET_GenericReturnValue +recoup_coin_lookup (void *cls, + const struct TALER_CoinSpendPublicKeyP *coin_pub, + const struct TALER_DenominationHashP **h_denom_pub, + const struct TALER_AgeCommitmentHashP **h_age_commitment) +{ + struct TALER_EXCHANGE_PostRecoupWithdrawHandle *prwh = cls; + + for (size_t i = 0; i < prwh->num_recouped; i++) + { + if (0 != GNUNET_memcmp (coin_pub, + &prwh->recouped_pubs[i])) + continue; + *h_denom_pub = &prwh->recouped_infos[i].h_denom_pub; + *h_age_commitment = prwh->recouped_infos[i].have_age + ? &prwh->recouped_infos[i].h_age_commitment + : NULL; + return GNUNET_OK; + } + return GNUNET_NO; +} + + +/** * Function called when we're done processing the * HTTP /recoup-withdraw request. * @@ -261,10 +299,30 @@ handle_recoup_withdraw_finished (void *cls, rr.hr.hint = TALER_JSON_get_error_hint (j); break; case MHD_HTTP_CONFLICT: - /* Commitment mismatch, denomination mismatch or a coin without - residual value; pass the JSON reply to the application */ rr.hr.ec = TALER_JSON_get_error_code (j); rr.hr.hint = TALER_JSON_get_error_hint (j); + switch (rr.hr.ec) + { + case TALER_EC_EXCHANGE_GENERIC_COIN_CONFLICTING_DENOMINATION_KEY: + case TALER_EC_EXCHANGE_GENERIC_COIN_CONFLICTING_AGE_HASH: + if (GNUNET_OK != + TALER_EXCHANGE_check_coin_conflict_ (prwh->keys, + rr.hr.ec, + j, + &recoup_coin_lookup, + prwh, + &rr.details.conflict)) + { + GNUNET_break_op (0); + rr.hr.http_status = 0; + rr.hr.ec = TALER_EC_GENERIC_REPLY_MALFORMED; + } + break; + default: + /* Commitment mismatch, denomination mismatch or a coin without + residual value; pass the JSON reply to the application */ + break; + } break; case MHD_HTTP_GONE: /* Denomination not eligible for recoup (not revoked, or expired) */ @@ -301,6 +359,8 @@ handle_recoup_withdraw_finished (void *cls, } prwh->cb (prwh->cb_cls, &rr); + TALER_EXCHANGE_free_coin_conflict_ (&rr.hr, + &rr.details.conflict); TALER_EXCHANGE_post_recoup_withdraw_cancel (prwh); } @@ -322,16 +382,21 @@ TALER_EXCHANGE_post_recoup_withdraw_create ( prwh = GNUNET_new (struct TALER_EXCHANGE_PostRecoupWithdrawHandle); prwh->recouped_pubs = GNUNET_new_array (num_coins, struct TALER_CoinSpendPublicKeyP); + prwh->recouped_infos = GNUNET_new_array ( + num_coins, + struct TALER_EXCHANGE_RecoupedCoinInfo_); coin_data = TALER_EXCHANGE_recoup_coin_data_ (num_coins, coins, blinding_seed, false, /* not for melt */ prwh->recouped_pubs, + prwh->recouped_infos, &prwh->num_recouped); if (NULL == coin_data) { GNUNET_break (0); GNUNET_free (prwh->recouped_pubs); + GNUNET_free (prwh->recouped_infos); GNUNET_free (prwh); return NULL; } @@ -409,6 +474,7 @@ TALER_EXCHANGE_post_recoup_withdraw_cancel ( GNUNET_free (prwh->url); GNUNET_free (prwh->base_url); GNUNET_free (prwh->recouped_pubs); + GNUNET_free (prwh->recouped_infos); json_decref (prwh->body); TALER_EXCHANGE_keys_decref (prwh->keys); GNUNET_free (prwh); diff --git a/src/lib/exchange_api_post-reserves-RESERVE_PUB-open.c b/src/lib/exchange_api_post-reserves-RESERVE_PUB-open.c @@ -52,6 +52,16 @@ struct CoinData struct TALER_DenominationHashP h_denom_pub; /** + * Hash of the age commitment of the coin, if @e have_age. + */ + struct TALER_AgeCommitmentHashP h_age_commitment; + + /** + * True if the coin has an age commitment. + */ + bool have_age; + + /** * How much did this coin contribute. */ struct TALER_Amount contribution; @@ -211,6 +221,41 @@ handle_reserves_open_pr (struct TALER_EXCHANGE_PostReservesOpenHandle *proh, /** + * Find the coin @a coin_pub paid into the reserve and how we used it. + * + * @param cls a `struct TALER_EXCHANGE_PostReservesOpenHandle *` + * @param coin_pub public key of the coin named in the conflict reply + * @param[out] h_denom_pub set to the hash of the denomination we used + * @param[out] h_age_commitment set to the age commitment hash we used, or NULL + * @return #GNUNET_OK if found, #GNUNET_NO if the coin is not ours + */ +static enum GNUNET_GenericReturnValue +reserve_open_coin_lookup (void *cls, + const struct TALER_CoinSpendPublicKeyP *coin_pub, + const struct TALER_DenominationHashP **h_denom_pub, + const struct TALER_AgeCommitmentHashP ** + h_age_commitment) +{ + struct TALER_EXCHANGE_PostReservesOpenHandle *proh = cls; + + for (unsigned int i = 0; i < proh->num_coins; i++) + { + const struct CoinData *cd = &proh->coins[i]; + + if (0 != GNUNET_memcmp (coin_pub, + &cd->coin_pub)) + continue; + *h_denom_pub = &cd->h_denom_pub; + *h_age_commitment = cd->have_age + ? &cd->h_age_commitment + : NULL; + return GNUNET_OK; + } + return GNUNET_NO; +} + + +/** * Function called when we're done processing the * HTTP /reserves/$RID/open request. * @@ -319,6 +364,29 @@ handle_reserves_open_finished (void *cls, } rs.hr.ec = TALER_JSON_get_error_code (j); rs.hr.hint = TALER_JSON_get_error_hint (j); + switch (rs.hr.ec) + { + case TALER_EC_EXCHANGE_GENERIC_COIN_CONFLICTING_DENOMINATION_KEY: + case TALER_EC_EXCHANGE_GENERIC_COIN_CONFLICTING_AGE_HASH: + if (GNUNET_OK != + TALER_EXCHANGE_check_coin_conflict_ (proh->keys, + rs.hr.ec, + j, + &reserve_open_coin_lookup, + proh, + &rs.details.conflict. + coin_conflict)) + { + GNUNET_break_op (0); + rs.hr.http_status = 0; + rs.hr.ec = TALER_EC_GENERIC_REPLY_MALFORMED; + } + break; + default: + /* insufficient funds: proof is the coin history, checked by + the application via GET /coins/$COIN_PUB/history */ + break; + } break; } case MHD_HTTP_INTERNAL_SERVER_ERROR: @@ -344,6 +412,8 @@ handle_reserves_open_finished (void *cls, &rs); proh->cb = NULL; } + TALER_EXCHANGE_free_coin_conflict_ (&rs.hr, + &rs.details.conflict.coin_conflict); TALER_EXCHANGE_post_reserves_open_cancel (proh); } @@ -400,6 +470,8 @@ TALER_EXCHANGE_post_reserves_open_create ( TALER_age_commitment_hash (&acp->commitment, &ahac); achp = &ahac; + cd->h_age_commitment = ahac; + cd->have_age = true; } TALER_wallet_reserve_open_deposit_sign (&pd->amount, &pd->h_denom_pub, diff --git a/src/lib/meson.build b/src/lib/meson.build @@ -147,3 +147,25 @@ test( suite: ['stefan', 'installcheck'], ) +test_coin_conflict = executable( + 'test_coin_conflict', + ['test_coin_conflict.c'], + dependencies: [ + gnunetutil_dep, + gnunetjson_dep, + json_dep, + libtalerutil_dep, + libtalerjson_dep, + libtalerexchange_dep, + ], + include_directories: [incdir, configuration_inc], + build_by_default: false, + install: false, +) +test( + 'test_coin_conflict', + test_coin_conflict, + workdir: meson.current_build_dir(), + suite: ['lib'], +) + diff --git a/src/lib/test_coin_conflict.c b/src/lib/test_coin_conflict.c @@ -0,0 +1,599 @@ +/* + This file is part of TALER + Copyright (C) 2026 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 lib/test_coin_conflict.c + * @brief test the client-side checks of coin conflict proofs + * (#TALER_EXCHANGE_check_coin_denomination_conflict_() and + * #TALER_EXCHANGE_check_coin_age_commitment_conflict_()) + * against replies built the way the exchange builds them + * @author Özgür Kesim + */ +#include "platform.h" +#include "taler/taler_util.h" +#include "taler/taler_json_lib.h" +#include "taler/taler_exchange_service.h" +#include "exchange_api_common.h" + + +/** + * Age mask of the age-restricted test denomination. + */ +static const struct TALER_AgeMask age_mask = { + .bits = 1 | 1 << 8 | 1 << 10 | 1 << 12 + | 1 << 14 | 1 << 16 | 1 << 18 | 1 << 21 +}; + + +/** + * A test denomination. + */ +struct Denom +{ + struct TALER_DenominationPrivateKey priv; + struct TALER_DenominationPublicKey pub; + struct TALER_DenominationHashP h; +}; + + +/** + * A coin issued by a test denomination. + */ +struct Coin +{ + struct TALER_CoinSpendPublicKeyP pub; + struct TALER_DenominationSignature sig; +}; + + +/** + * Create an RSA test denomination. + * + * @param age_restricted whether the denomination has an age mask + * @param[out] d the denomination + */ +static void +make_denom (bool age_restricted, + struct Denom *d) +{ + GNUNET_assert (GNUNET_OK == + TALER_denom_priv_create (&d->priv, + &d->pub, + GNUNET_CRYPTO_BSA_RSA, + 1024)); + if (age_restricted) + d->pub.age_mask = age_mask; + TALER_denom_pub_hash (&d->pub, + &d->h); +} + + +/** + * Issue a coin: a fresh key, blinded, signed by @a d and unblinded. + * + * @param d the issuing denomination + * @param ach age commitment hash to bind into the coin, NULL for none + * @param[out] c the coin + */ +static void +make_coin (const struct Denom *d, + const struct TALER_AgeCommitmentHashP *ach, + struct Coin *c) +{ + struct TALER_PlanchetMasterSecretP ps; + struct TALER_CoinSpendPrivateKeyP coin_priv; + union GNUNET_CRYPTO_BlindingSecretP bks; + const struct TALER_ExchangeBlindingValues *alg_values; + struct TALER_PlanchetDetail pd; + struct TALER_BlindedDenominationSignature blind_sig; + struct TALER_FreshCoin coin; + struct TALER_CoinPubHashP c_hash; + + alg_values = TALER_denom_ewv_rsa_singleton (); + GNUNET_CRYPTO_random_block (&ps, + sizeof (ps)); + TALER_planchet_setup_coin_priv (&ps, + alg_values, + &coin_priv); + TALER_planchet_blinding_secret_create (&ps, + alg_values, + &bks); + GNUNET_assert (GNUNET_OK == + TALER_planchet_prepare (&d->pub, + alg_values, + &bks, + NULL, + &coin_priv, + ach, + &c_hash, + &pd)); + GNUNET_assert (GNUNET_OK == + TALER_denom_sign_blinded (&blind_sig, + &d->priv, + false, + &pd.blinded_planchet)); + TALER_planchet_detail_free (&pd); + GNUNET_assert (GNUNET_OK == + TALER_planchet_to_coin (&d->pub, + &blind_sig, + &bks, + &coin_priv, + ach, + &c_hash, + alg_values, + &coin)); + TALER_blinded_denom_sig_free (&blind_sig); + GNUNET_CRYPTO_eddsa_key_get_public (&coin_priv.eddsa_priv, + &c->pub.eddsa_pub); + c->sig = coin.sig; +} + + +/** + * Build a denomination conflict reply as the exchange does. + * + * @param c the coin + * @param prev the denomination the exchange knows the coin under + * @param prev_ach age commitment hash stored for the coin, NULL for none + * @return the reply body + */ +static json_t * +denom_conflict_reply (const struct Coin *c, + const struct Denom *prev, + const struct TALER_AgeCommitmentHashP *prev_ach) +{ + json_t *j; + + j = GNUNET_JSON_PACK ( + TALER_JSON_pack_ec ( + TALER_EC_EXCHANGE_GENERIC_COIN_CONFLICTING_DENOMINATION_KEY), + GNUNET_JSON_pack_data_auto ("coin_pub", + &c->pub), + TALER_JSON_pack_denom_pub ("prev_denom_pub", + &prev->pub), + TALER_JSON_pack_denom_sig ("prev_denom_sig", + &c->sig), + GNUNET_JSON_pack_allow_null ( + GNUNET_JSON_pack_data_auto ("prev_h_age_commitment", + prev_ach))); + GNUNET_assert (NULL != j); + return j; +} + + +/** + * Build an age commitment conflict reply as the exchange does. + * + * @param c the coin + * @param d the denomination of the coin + * @param expected age commitment hash stored for the coin, NULL for none + * @return the reply body + */ +static json_t * +age_conflict_reply (const struct Coin *c, + const struct Denom *d, + const struct TALER_AgeCommitmentHashP *expected) +{ + json_t *j; + + j = GNUNET_JSON_PACK ( + TALER_JSON_pack_ec ( + TALER_EC_EXCHANGE_GENERIC_COIN_CONFLICTING_AGE_HASH), + GNUNET_JSON_pack_data_auto ("coin_pub", + &c->pub), + GNUNET_JSON_pack_data_auto ("h_denom_pub", + &d->h), + GNUNET_JSON_pack_allow_null ( + GNUNET_JSON_pack_data_auto ("expected_age_commitment_hash", + expected)), + GNUNET_JSON_pack_string ("conflict_detail", + "test"), + TALER_JSON_pack_denom_sig ("prev_denom_sig", + &c->sig)); + GNUNET_assert (NULL != j); + return j; +} + + +/** + * Run the denomination conflict check on @a j for a client that used + * @a used. + * + * @param keys exchange keys + * @param j reply to check + * @param used denomination hash the client used + * @param[out] verified set to the verified flag of the parsed reply + * @return result of the check, #GNUNET_SYSERR also if parsing failed + */ +static enum GNUNET_GenericReturnValue +run_denom_check (const struct TALER_EXCHANGE_Keys *keys, + json_t *j, + const struct TALER_DenominationHashP *used, + bool *verified) +{ + struct TALER_EXCHANGE_CoinDenominationConflict cdc; + enum GNUNET_GenericReturnValue ret; + + *verified = false; + if (GNUNET_OK != + TALER_EXCHANGE_parse_coin_denomination_conflict_ (j, + &cdc)) + { + json_decref (j); + return GNUNET_SYSERR; + } + ret = TALER_EXCHANGE_check_coin_denomination_conflict_ (keys, + used, + &cdc); + *verified = cdc.verified; + TALER_EXCHANGE_free_coin_denomination_conflict_ (&cdc); + json_decref (j); + return ret; +} + + +/** + * Run the age commitment conflict check on @a j for a client that + * used @a used_denom and @a used_ach. + * + * @param keys exchange keys + * @param j reply to check + * @param used_denom denomination hash the client used + * @param used_ach age commitment hash the client used, NULL for none + * @param[out] verified set to the verified flag of the parsed reply + * @return result of the check, #GNUNET_SYSERR also if parsing failed + */ +static enum GNUNET_GenericReturnValue +run_age_check (const struct TALER_EXCHANGE_Keys *keys, + json_t *j, + const struct TALER_DenominationHashP *used_denom, + const struct TALER_AgeCommitmentHashP *used_ach, + bool *verified) +{ + struct TALER_EXCHANGE_CoinAgeCommitmentConflict cac; + enum GNUNET_GenericReturnValue ret; + + *verified = false; + if (GNUNET_OK != + TALER_EXCHANGE_parse_coin_age_commitment_conflict_ (j, + &cac)) + { + json_decref (j); + return GNUNET_SYSERR; + } + ret = TALER_EXCHANGE_check_coin_age_commitment_conflict_ (keys, + used_denom, + used_ach, + &cac); + *verified = cac.verified; + TALER_EXCHANGE_free_coin_age_commitment_conflict_ (&cac); + json_decref (j); + return ret; +} + + +#define CHECK(cond) do { \ + if (! (cond)) \ + { \ + fprintf (stderr, \ + "FAILED: %s at %s:%u\n", \ + #cond, __FILE__, __LINE__); \ + return 1; \ + } \ +} while (0) + + +/** + * Denomination conflicts: a coin issued by @a plain is claimed under + * @a aged (valid proof), under @a plain itself (no conflict), by an + * exchange we do not know the denomination of (unverifiable), with a + * tampered coin key (bad signature), and the age hash consistency + * check for a coin issued by @a aged. + * + * @param keys keys listing both denominations + * @param keys_aged keys listing only @a aged + * @param plain denomination without age restriction + * @param aged age-restricted denomination + * @return 0 on success + */ +static int +test_denomination_conflicts (const struct TALER_EXCHANGE_Keys *keys, + const struct TALER_EXCHANGE_Keys *keys_aged, + const struct Denom *plain, + const struct Denom *aged) +{ + struct Coin c; + struct Coin ca; + struct TALER_AgeCommitmentHashP ach; + struct TALER_AgeCommitmentHashP other; + bool verified; + json_t *j; + + GNUNET_CRYPTO_random_block (&ach, + sizeof (ach)); + GNUNET_CRYPTO_random_block (&other, + sizeof (other)); + make_coin (plain, + NULL, + &c); + make_coin (aged, + &ach, + &ca); + + /* valid proof: known under 'plain', client used 'aged' */ + CHECK (GNUNET_OK == + run_denom_check (keys, + denom_conflict_reply (&c, + plain, + NULL), + &aged->h, + &verified)); + CHECK (verified); + /* same denomination: not a conflict */ + CHECK (GNUNET_SYSERR == + run_denom_check (keys, + denom_conflict_reply (&c, + plain, + NULL), + &plain->h, + &verified)); + /* 'plain' is not in our keys: accepted, unverifiable */ + CHECK (GNUNET_NO == + run_denom_check (keys_aged, + denom_conflict_reply (&c, + plain, + NULL), + &aged->h, + &verified)); + CHECK (! verified); + /* tampered coin key: signature does not verify */ + j = denom_conflict_reply (&c, + plain, + NULL); + GNUNET_assert (0 == + json_object_set_new (j, + "coin_pub", + GNUNET_JSON_from_data_auto (&ca.pub))); + CHECK (GNUNET_SYSERR == + run_denom_check (keys, + j, + &aged->h, + &verified)); + /* age hash claimed for a denomination without age restriction */ + CHECK (GNUNET_SYSERR == + run_denom_check (keys, + denom_conflict_reply (&c, + plain, + &ach), + &aged->h, + &verified)); + /* valid proof with age hash: known under 'aged', client used 'plain' */ + CHECK (GNUNET_OK == + run_denom_check (keys, + denom_conflict_reply (&ca, + aged, + &ach), + &plain->h, + &verified)); + CHECK (verified); + /* age hash missing for an age-restricted denomination */ + CHECK (GNUNET_SYSERR == + run_denom_check (keys, + denom_conflict_reply (&ca, + aged, + NULL), + &plain->h, + &verified)); + /* wrong age hash: signature does not verify */ + CHECK (GNUNET_SYSERR == + run_denom_check (keys, + denom_conflict_reply (&ca, + aged, + &other), + &plain->h, + &verified)); + /* malformed: prev_denom_sig missing */ + j = denom_conflict_reply (&c, + plain, + NULL); + GNUNET_assert (0 == + json_object_del (j, + "prev_denom_sig")); + CHECK (GNUNET_SYSERR == + run_denom_check (keys, + j, + &aged->h, + &verified)); + TALER_denom_sig_free (&c.sig); + TALER_denom_sig_free (&ca.sig); + return 0; +} + + +/** + * Age commitment conflicts: a coin issued by @a aged with hash X is + * claimed with hash Y (valid), with X (no conflict), without a hash + * (valid), under another denomination (invalid), by an exchange we do + * not know the denomination of (unverifiable), with a wrong stored + * hash (bad signature); and a coin issued by @a plain without a hash + * claimed with one. + * + * @param keys keys listing both denominations + * @param keys_plain keys listing only @a plain + * @param plain denomination without age restriction + * @param aged age-restricted denomination + * @return 0 on success + */ +static int +test_age_conflicts (const struct TALER_EXCHANGE_Keys *keys, + const struct TALER_EXCHANGE_Keys *keys_plain, + const struct Denom *plain, + const struct Denom *aged) +{ + struct Coin c; + struct Coin ca; + struct TALER_AgeCommitmentHashP x; + struct TALER_AgeCommitmentHashP y; + bool verified; + + GNUNET_CRYPTO_random_block (&x, + sizeof (x)); + GNUNET_CRYPTO_random_block (&y, + sizeof (y)); + make_coin (aged, + &x, + &ca); + make_coin (plain, + NULL, + &c); + + /* valid: stored X, client used Y */ + CHECK (GNUNET_OK == + run_age_check (keys, + age_conflict_reply (&ca, + aged, + &x), + &aged->h, + &y, + &verified)); + CHECK (verified); + /* same hash: not a conflict */ + CHECK (GNUNET_SYSERR == + run_age_check (keys, + age_conflict_reply (&ca, + aged, + &x), + &aged->h, + &x, + &verified)); + /* valid: stored X, client used none */ + CHECK (GNUNET_OK == + run_age_check (keys, + age_conflict_reply (&ca, + aged, + &x), + &aged->h, + NULL, + &verified)); + CHECK (verified); + /* different denomination: that would be a denomination conflict */ + CHECK (GNUNET_SYSERR == + run_age_check (keys, + age_conflict_reply (&ca, + aged, + &x), + &plain->h, + &y, + &verified)); + /* 'aged' is not in our keys: accepted, unverifiable */ + CHECK (GNUNET_NO == + run_age_check (keys_plain, + age_conflict_reply (&ca, + aged, + &x), + &aged->h, + &y, + &verified)); + CHECK (! verified); + /* stored hash claimed to be Y: signature does not verify */ + CHECK (GNUNET_SYSERR == + run_age_check (keys, + age_conflict_reply (&ca, + aged, + &y), + &aged->h, + &x, + &verified)); + /* valid: stored none, client used Y */ + CHECK (GNUNET_OK == + run_age_check (keys, + age_conflict_reply (&c, + plain, + NULL), + &plain->h, + &y, + &verified)); + CHECK (verified); + /* stored none, client used none: not a conflict */ + CHECK (GNUNET_SYSERR == + run_age_check (keys, + age_conflict_reply (&c, + plain, + NULL), + &plain->h, + NULL, + &verified)); + TALER_denom_sig_free (&c.sig); + TALER_denom_sig_free (&ca.sig); + return 0; +} + + +int +main (int argc, + const char *const argv[]) +{ + struct Denom plain; + struct Denom aged; + struct TALER_EXCHANGE_DenomPublicKey dks[2]; + struct TALER_EXCHANGE_Keys keys = { + .denom_keys = dks, + .num_denom_keys = 2 + }; + struct TALER_EXCHANGE_Keys keys_plain = { + .denom_keys = &dks[0], + .num_denom_keys = 1 + }; + struct TALER_EXCHANGE_Keys keys_aged = { + .denom_keys = &dks[1], + .num_denom_keys = 1 + }; + int ret; + + (void) argc; + (void) argv; + GNUNET_log_setup ("test-coin-conflict", + "WARNING", + NULL); + make_denom (false, + &plain); + make_denom (true, + &aged); + memset (dks, + 0, + sizeof (dks)); + dks[0].key = plain.pub; + dks[0].h_key = plain.h; + dks[1].key = aged.pub; + dks[1].h_key = aged.h; + + ret = test_denomination_conflicts (&keys, + &keys_aged, + &plain, + &aged); + if (0 == ret) + ret = test_age_conflicts (&keys, + &keys_plain, + &plain, + &aged); + TALER_denom_priv_free (&plain.priv); + TALER_denom_pub_free (&plain.pub); + TALER_denom_priv_free (&aged.priv); + TALER_denom_pub_free (&aged.pub); + return ret; +} + + +/* end of test_coin_conflict.c */ diff --git a/src/testing/testing_api_cmd_deposit.c b/src/testing/testing_api_cmd_deposit.c @@ -284,6 +284,25 @@ deposit_cb (struct DepositState *ds, return; } + if ( (MHD_HTTP_CONFLICT == dr->hr.http_status) && + (TALER_EC_EXCHANGE_GENERIC_COIN_CONFLICTING_DENOMINATION_KEY == + dr->hr.ec) ) + { + const struct TALER_EXCHANGE_CoinDenominationConflict *cdc + = &dr->details.conflict.details.coin_conflict.details. + denomination_conflict; + + /* the library verified the exchange's proof; the denomination it + names must be one we know, and differ from the one we used */ + if ( (! cdc->verified) || + (0 == GNUNET_memcmp (&cdc->prev_h_denom_pub, + &ds->denom_pub->h_key)) ) + { + GNUNET_break (0); + TALER_TESTING_interpreter_fail (ds->is); + return; + } + } if (MHD_HTTP_OK == dr->hr.http_status) { ds->deposit_succeeded = true; diff --git a/src/testing/testing_api_cmd_refresh.c b/src/testing/testing_api_cmd_refresh.c @@ -705,6 +705,24 @@ melt_cb (struct MeltState *ms, hr->reply); return; } + if ( (MHD_HTTP_CONFLICT == hr->http_status) && + (TALER_EC_EXCHANGE_GENERIC_COIN_CONFLICTING_DENOMINATION_KEY == + hr->ec) ) + { + const struct TALER_EXCHANGE_CoinDenominationConflict *cdc + = &mr->details.conflict.details.denomination_conflict; + + /* the library verified the exchange's proof; the denomination it + names must be one we know, and differ from the one we used */ + if ( (! cdc->verified) || + (0 == GNUNET_memcmp (&cdc->prev_h_denom_pub, + &ms->melt_input.melt_pk.h_key)) ) + { + GNUNET_break (0); + TALER_TESTING_interpreter_fail (ms->is); + return; + } + } if (MHD_HTTP_OK == hr->http_status) { ms->noreveal_index = mr->details.ok.noreveal_index;