/* This file is part of TALER Copyright (C) 2023 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 */ /** * @file testing/testing_api_cmd_age_withdraw.c * @brief implements the age-withdraw command * @author Özgür Kesim */ #include "platform.h" #include "taler_exchange_service.h" #include "taler_json_lib.h" #include #include #include #include "taler_signatures.h" #include "taler_extensions.h" #include "taler_testing_lib.h" /* * The output state of coin */ struct CoinOutputState { /** * The calculated details during "age-withdraw", for the selected coin. */ struct TALER_EXCHANGE_AgeWithdrawCoinPrivateDetails details; /** * The (wanted) value of the coin, MUST be the same as input.denom_pub.value; */ struct TALER_Amount amount; /** * Reserve history entry that corresponds to this coin. * Will be of type #TALER_EXCHANGE_RTT_AGEWITHDRAWAL. */ struct TALER_EXCHANGE_ReserveHistoryEntry reserve_history; }; /** * State for a "age withdraw" CMD: */ struct AgeWithdrawState { /** * Interpreter state (during command) */ struct TALER_TESTING_Interpreter *is; /** * The age-withdraw handle */ struct TALER_EXCHANGE_AgeWithdrawHandle *handle; /** * Exchange base URL. Only used as offered trait. */ char *exchange_url; /** * URI of the reserve we are withdrawing from. */ char *reserve_payto_uri; /** * Private key of the reserve we are withdrawing from. */ struct TALER_ReservePrivateKeyP reserve_priv; /** * Public key of the reserve we are withdrawing from. */ struct TALER_ReservePublicKeyP reserve_pub; /** * Which reserve should we withdraw from? */ const char *reserve_reference; /** * Expected HTTP response code to the request. */ unsigned int expected_response_code; /** * Age mask */ struct TALER_AgeMask mask; /** * The maximum age we commit to */ uint8_t max_age; /** * Number of coins to withdraw */ size_t num_coins; /** * The @e num_coins input that is provided to the * `TALER_EXCHANGE_age_withdraw` API. * Each contains kappa secrets, from which we will have * to disclose kappa-1 in a subsequent age-withdraw-reveal operation. */ struct TALER_EXCHANGE_AgeWithdrawCoinInput *coin_inputs; /** * The output state of @e num_coins coins, calculated during the * "age-withdraw" operation. */ struct CoinOutputState *coin_outputs; /** * The index returned by the exchange for the "age-withdraw" operation, * of the kappa coin candidates that we do not disclose and keep. */ uint8_t noreveal_index; /** * The blinded hashes of the non-revealed (to keep) @e num_coins coins. */ const struct TALER_BlindedCoinHashP *blinded_coin_hs; /** * The hash of the commitment, needed for the reveal step. */ struct TALER_AgeWithdrawCommitmentHashP h_commitment; /** * Set to the KYC requirement payto hash *if* the exchange replied with a * request for KYC. */ struct TALER_PaytoHashP h_payto; /** * Set to the KYC requirement row *if* the exchange replied with * a request for KYC. */ uint64_t requirement_row; }; /** * Callback for the "age-withdraw" ooperation; It checks that the response * code is expected and store the exchange signature in the state. * * @param cls Closure of type `struct AgeWithdrawState *` * @param response Repsonse details */ static void age_withdraw_cb ( void *cls, const struct TALER_EXCHANGE_AgeWithdrawResponse *response) { struct AgeWithdrawState *aws = cls; struct TALER_TESTING_Interpreter *is = aws->is; aws->handle = NULL; if (aws->expected_response_code != response->hr.http_status) { TALER_TESTING_unexpected_status_with_body (is, response->hr.http_status, aws->expected_response_code, response->hr.reply); return; } switch (response->hr.http_status) { case MHD_HTTP_OK: aws->noreveal_index = response->details.ok.noreveal_index; aws->h_commitment = response->details.ok.h_commitment; GNUNET_assert (aws->num_coins == response->details.ok.num_coins); for (size_t n = 0; n < aws->num_coins; n++) { aws->coin_outputs[n].details = response->details.ok.coin_details[n]; TALER_age_commitment_proof_deep_copy ( &response->details.ok.coin_details[n].age_commitment_proof, &aws->coin_outputs[n].details.age_commitment_proof); } aws->blinded_coin_hs = response->details.ok.blinded_coin_hs; break; case MHD_HTTP_FORBIDDEN: case MHD_HTTP_NOT_FOUND: case MHD_HTTP_GONE: /* nothing to check */ break; case MHD_HTTP_CONFLICT: /* TODO[oec]: Add this to the response-type and handle it here */ break; case MHD_HTTP_UNAVAILABLE_FOR_LEGAL_REASONS: default: /* Unsupported status code (by test harness) */ GNUNET_log (GNUNET_ERROR_TYPE_WARNING, "test command for age-withdraw not support status code %u, body:\n" ">>%s<<\n", response->hr.http_status, json_dumps (response->hr.reply, JSON_INDENT (2))); GNUNET_break (0); break; } /* We are done with this command, pick the next one */ TALER_TESTING_interpreter_next (is); } /** * Run the command for age-withdraw. */ static void age_withdraw_run ( void *cls, const struct TALER_TESTING_Command *cmd, struct TALER_TESTING_Interpreter *is) { struct AgeWithdrawState *aws = cls; struct TALER_EXCHANGE_Keys *keys = TALER_TESTING_get_keys (is); const struct TALER_ReservePrivateKeyP *rp; const struct TALER_TESTING_Command *create_reserve; const struct TALER_EXCHANGE_DenomPublicKey *dpk; aws->is = is; /* Prepare the reserve related data */ create_reserve = TALER_TESTING_interpreter_lookup_command ( is, aws->reserve_reference); if (NULL == create_reserve) { GNUNET_break (0); TALER_TESTING_interpreter_fail (is); return; } if (GNUNET_OK != TALER_TESTING_get_trait_reserve_priv (create_reserve, &rp)) { GNUNET_break (0); TALER_TESTING_interpreter_fail (is); return; } if (NULL == aws->exchange_url) aws->exchange_url = GNUNET_strdup (TALER_TESTING_get_exchange_url (is)); aws->reserve_priv = *rp; GNUNET_CRYPTO_eddsa_key_get_public (&aws->reserve_priv.eddsa_priv, &aws->reserve_pub.eddsa_pub); aws->reserve_payto_uri = TALER_reserve_make_payto (aws->exchange_url, &aws->reserve_pub); aws->coin_inputs = GNUNET_new_array ( aws->num_coins, struct TALER_EXCHANGE_AgeWithdrawCoinInput); for (unsigned int i = 0; inum_coins; i++) { struct TALER_EXCHANGE_AgeWithdrawCoinInput *input = &aws->coin_inputs[i]; struct CoinOutputState *cos = &aws->coin_outputs[i]; /* randomly create the secrets for the kappa coin-candidates */ GNUNET_CRYPTO_random_block (GNUNET_CRYPTO_QUALITY_WEAK, &input->secrets, sizeof(input->secrets)); /* Find denomination */ dpk = TALER_TESTING_find_pk (keys, &cos->amount, true); /* _always_ use denominations with age-striction */ if (NULL == dpk) { GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Failed to determine denomination key for amount at %s\n", (NULL != cmd) ? cmd->label : ""); GNUNET_break (0); TALER_TESTING_interpreter_fail (is); return; } /* We copy the denomination key, as re-querying /keys * would free the old one. */ input->denom_pub = TALER_EXCHANGE_copy_denomination_key (dpk); cos->reserve_history.type = TALER_EXCHANGE_RTT_AGEWITHDRAWAL; GNUNET_assert (0 <= TALER_amount_add (&cos->reserve_history.amount, &cos->amount, &input->denom_pub->fees.withdraw)); cos->reserve_history.details.withdraw.fee = input->denom_pub->fees.withdraw; } /* Execute the age-withdraw protocol */ aws->handle = TALER_EXCHANGE_age_withdraw ( TALER_TESTING_interpreter_get_context (is), keys, TALER_TESTING_get_exchange_url (is), rp, aws->num_coins, aws->coin_inputs, aws->max_age, &age_withdraw_cb, aws); if (NULL == aws->handle) { GNUNET_break (0); TALER_TESTING_interpreter_fail (is); return; } } /** * Free the state of a "age withdraw" CMD, and possibly cancel a * pending operation thereof * * @param cls Closure of type `struct AgeWithdrawState` * @param cmd The command beeing freed. */ static void age_withdraw_cleanup ( void *cls, const struct TALER_TESTING_Command *cmd) { struct AgeWithdrawState *aws = cls; if (NULL != aws->handle) { TALER_TESTING_command_incomplete (aws->is, cmd->label); TALER_EXCHANGE_age_withdraw_cancel (aws->handle); aws->handle = NULL; } for (size_t n = 0; n < aws->num_coins; n++) { struct TALER_EXCHANGE_AgeWithdrawCoinInput *in = &aws->coin_inputs[n]; struct CoinOutputState *out = &aws->coin_outputs[n]; if (NULL != in && NULL != in->denom_pub) { TALER_EXCHANGE_destroy_denomination_key (in->denom_pub); in->denom_pub = NULL; } if (NULL != out) TALER_age_commitment_proof_free (&out->details.age_commitment_proof); } GNUNET_free (aws->coin_inputs); GNUNET_free (aws->coin_outputs); GNUNET_free (aws->exchange_url); GNUNET_free (aws->reserve_payto_uri); GNUNET_free (aws); } /** * Offer internal data of a "age withdraw" CMD state to other commands. * * @param cls Closure of type `struct AgeWithdrawState` * @param[out] ret result (could be anything) * @param trait name of the trait * @param idx index number of the object to offer. * @return #GNUNET_OK on success */ static enum GNUNET_GenericReturnValue age_withdraw_traits ( void *cls, const void **ret, const char *trait, unsigned int idx) { struct AgeWithdrawState *aws = cls; uint8_t k = aws->noreveal_index; struct TALER_EXCHANGE_AgeWithdrawCoinInput *in = &aws->coin_inputs[idx]; struct CoinOutputState *out = &aws->coin_outputs[idx]; struct TALER_EXCHANGE_AgeWithdrawCoinPrivateDetails *details = &aws->coin_outputs[idx].details; struct TALER_TESTING_Trait traits[] = { /* history entry MUST be first due to response code logic below! */ TALER_TESTING_make_trait_reserve_history (idx, &out->reserve_history), TALER_TESTING_make_trait_denom_pub (idx, in->denom_pub), TALER_TESTING_make_trait_reserve_priv (&aws->reserve_priv), TALER_TESTING_make_trait_reserve_pub (&aws->reserve_pub), TALER_TESTING_make_trait_amounts (idx, &out->amount), /* TODO[oec]: add legal requirement to response and handle it here, as well TALER_TESTING_make_trait_legi_requirement_row (&aws->requirement_row), TALER_TESTING_make_trait_h_payto (&aws->h_payto), */ TALER_TESTING_make_trait_h_blinded_coin (idx, &aws->blinded_coin_hs[idx]), TALER_TESTING_make_trait_payto_uri (aws->reserve_payto_uri), TALER_TESTING_make_trait_exchange_url (aws->exchange_url), TALER_TESTING_make_trait_coin_priv (idx, &details->coin_priv), TALER_TESTING_make_trait_planchet_secrets (idx, &in->secrets[k]), TALER_TESTING_make_trait_blinding_key (idx, &details->blinding_key), TALER_TESTING_make_trait_exchange_wd_value (idx, &details->alg_values), TALER_TESTING_make_trait_age_commitment_proof ( idx, &details->age_commitment_proof), TALER_TESTING_make_trait_h_age_commitment ( idx, &details->h_age_commitment), }; if (idx >= aws->num_coins) return GNUNET_NO; return TALER_TESTING_get_trait ((aws->expected_response_code == MHD_HTTP_OK) ? &traits[0] /* we have reserve history */ : &traits[1], /* skip reserve history */ ret, trait, idx); } struct TALER_TESTING_Command TALER_TESTING_cmd_age_withdraw (const char *label, const char *reserve_reference, uint8_t max_age, unsigned int expected_response_code, const char *amount, ...) { struct AgeWithdrawState *aws; unsigned int cnt; va_list ap; aws = GNUNET_new (struct AgeWithdrawState); aws->reserve_reference = reserve_reference; aws->expected_response_code = expected_response_code; aws->mask = TALER_extensions_get_age_restriction_mask (); aws->max_age = TALER_get_lowest_age (&aws->mask, max_age); cnt = 1; va_start (ap, amount); while (NULL != (va_arg (ap, const char *))) cnt++; aws->num_coins = cnt; aws->coin_outputs = GNUNET_new_array (cnt, struct CoinOutputState); va_end (ap); va_start (ap, amount); for (unsigned int i = 0; inum_coins; i++) { struct CoinOutputState *out = &aws->coin_outputs[i]; if (GNUNET_OK != TALER_string_to_amount (amount, &out->amount)) { GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Failed to parse amount `%s' at %s\n", amount, label); GNUNET_assert (0); } /* move on to next vararg! */ amount = va_arg (ap, const char *); } GNUNET_assert (NULL == amount); va_end (ap); { struct TALER_TESTING_Command cmd = { .cls = aws, .label = label, .run = &age_withdraw_run, .cleanup = &age_withdraw_cleanup, .traits = &age_withdraw_traits, }; return cmd; } } /** * The state for the age-withdraw-reveal operation */ struct AgeWithdrawRevealState { /** * The reference to the CMD resembling the previous call to age-withdraw */ const char *age_withdraw_reference; /** * The state to the previous age-withdraw command */ const struct AgeWithdrawState *aws; /** * The expected response code from the call to the * age-withdraw-reveal operation */ unsigned int expected_response_code; /** * Interpreter state (during command) */ struct TALER_TESTING_Interpreter *is; /** * The handle to the reveal-operation */ struct TALER_EXCHANGE_AgeWithdrawRevealHandle *handle; /** * Number of coins, extracted form the age withdraw command */ size_t num_coins; /** * The signatures of the @e num_coins coins returned */ struct TALER_DenominationSignature *denom_sigs; }; /* * Callback for the reveal response * * @param cls Closure of type `struct AgeWithdrawRevealState` * @param awr The response */ static void age_withdraw_reveal_cb ( void *cls, const struct TALER_EXCHANGE_AgeWithdrawRevealResponse *response) { struct AgeWithdrawRevealState *awrs = cls; struct TALER_TESTING_Interpreter *is = awrs->is; awrs->handle = NULL; if (awrs->expected_response_code != response->hr.http_status) { TALER_TESTING_unexpected_status_with_body (is, response->hr.http_status, awrs->expected_response_code, response->hr.reply); return; } switch (response->hr.http_status) { case MHD_HTTP_OK: { const struct AgeWithdrawState *aws = awrs->aws; GNUNET_assert (awrs->num_coins == response->details.ok.num_sigs); awrs->denom_sigs = GNUNET_new_array (awrs->num_coins, struct TALER_DenominationSignature); for (size_t n = 0; n < awrs->num_coins; n++) TALER_denom_sig_unblind (&awrs->denom_sigs[n], &response->details.ok.blinded_denom_sigs[n], &aws->coin_outputs[n].details.blinding_key, &aws->coin_outputs[n].details.h_coin_pub, &aws->coin_outputs[n].details.alg_values, &aws->coin_inputs[n].denom_pub->key); GNUNET_log (GNUNET_ERROR_TYPE_INFO, "age-withdraw reveal success!\n"); } break; case MHD_HTTP_NOT_FOUND: case MHD_HTTP_FORBIDDEN: /* nothing to check */ break; /* TODO[oec]: handle more cases !? */ default: /* Unsupported status code (by test harness) */ GNUNET_log (GNUNET_ERROR_TYPE_WARNING, "Age withdraw reveal test command does not support status code %u\n", response->hr.http_status); GNUNET_break (0); break; } /* We are done with this command, pick the next one */ TALER_TESTING_interpreter_next (is); } /** * Run the command for age-withdraw-reveal */ static void age_withdraw_reveal_run ( void *cls, const struct TALER_TESTING_Command *cmd, struct TALER_TESTING_Interpreter *is) { struct AgeWithdrawRevealState *awrs = cls; const struct TALER_TESTING_Command *age_withdraw_cmd; const struct AgeWithdrawState *aws; (void) cmd; awrs->is = is; /* * Get the command and state for the previous call to "age witdraw" */ age_withdraw_cmd = TALER_TESTING_interpreter_lookup_command (is, awrs->age_withdraw_reference); if (NULL == age_withdraw_cmd) { GNUNET_break (0); TALER_TESTING_interpreter_fail (is); } GNUNET_assert (age_withdraw_cmd->run == age_withdraw_run); aws = age_withdraw_cmd->cls; awrs->aws = aws; awrs->num_coins = aws->num_coins; awrs->handle = TALER_EXCHANGE_age_withdraw_reveal ( TALER_TESTING_interpreter_get_context (is), TALER_TESTING_get_exchange_url (is), aws->num_coins, aws->coin_inputs, aws->noreveal_index, &aws->h_commitment, &aws->reserve_pub, age_withdraw_reveal_cb, awrs); } /** * Free the state of a "age-withdraw-reveal" CMD, and possibly * cancel a pending operation thereof * * @param cls Closure of type `struct AgeWithdrawRevealState` * @param cmd The command being freed. */ static void age_withdraw_reveal_cleanup ( void *cls, const struct TALER_TESTING_Command *cmd) { struct AgeWithdrawRevealState *awrs = cls; if (NULL != awrs->handle) { TALER_TESTING_command_incomplete (awrs->is, cmd->label); TALER_EXCHANGE_age_withdraw_reveal_cancel (awrs->handle); awrs->handle = NULL; } GNUNET_free (awrs->denom_sigs); awrs->denom_sigs = NULL; GNUNET_free (awrs); } /** * Offer internal data of a "age withdraw reveal" CMD state to other commands. * * @param cls Closure of they `struct AgeWithdrawRevealState` * @param[out] ret result (could be anything) * @param trait name of the trait * @param idx index number of the object to offer. * @return #GNUNET_OK on success */ static enum GNUNET_GenericReturnValue age_withdraw_reveal_traits ( void *cls, const void **ret, const char *trait, unsigned int idx) { struct AgeWithdrawRevealState *awrs = cls; struct TALER_TESTING_Trait traits[] = { TALER_TESTING_make_trait_denom_sig (idx, &awrs->denom_sigs[idx]), /* FIXME: shall we provide the traits from the previous * call to "age withdraw" as well? */ }; if (idx >= awrs->num_coins) return GNUNET_NO; return TALER_TESTING_get_trait (traits, ret, trait, idx); } struct TALER_TESTING_Command TALER_TESTING_cmd_age_withdraw_reveal ( const char *label, const char *age_withdraw_reference, unsigned int expected_response_code) { struct AgeWithdrawRevealState *awrs = GNUNET_new (struct AgeWithdrawRevealState); awrs->age_withdraw_reference = age_withdraw_reference; awrs->expected_response_code = expected_response_code; struct TALER_TESTING_Command cmd = { .cls = awrs, .label = label, .run = age_withdraw_reveal_run, .cleanup = age_withdraw_reveal_cleanup, .traits = age_withdraw_reveal_traits, }; return cmd; } /* end of testing_api_cmd_age_withdraw.c */