From 7085cfef70889a11508cdac4cd887b9959f59218 Mon Sep 17 00:00:00 2001 From: Christian Grothoff Date: Fri, 10 Jul 2020 23:09:46 +0200 Subject: test coin_priv re-use with deposit and refresh, update handling of the error code client-side --- src/include/taler_testing_lib.h | 24 +++++++ src/lib/exchange_api_melt.c | 83 ++++++++++++++++++++++-- src/testing/test_exchange_api.c | 43 +++++++++--- src/testing/testing_api_cmd_withdraw.c | 115 ++++++++++++++++++++++++++++++++- 4 files changed, 249 insertions(+), 16 deletions(-) diff --git a/src/include/taler_testing_lib.h b/src/include/taler_testing_lib.h index c3278b55f..332b429f9 100644 --- a/src/include/taler_testing_lib.h +++ b/src/include/taler_testing_lib.h @@ -1295,6 +1295,30 @@ TALER_TESTING_cmd_withdraw_amount (const char *label, unsigned int expected_response_code); +/** + * Create a withdraw command, letting the caller specify + * the desired amount as string and also re-using an existing + * coin private key in the process (violating the specification, + * which will result in an error when spending the coin!). + * + * @param label command label. + * @param reserve_reference command providing us with a reserve to withdraw from + * @param amount how much we withdraw. + * @param coin_ref reference to (withdraw/reveal) command of a coin + * from which we should re-use the private key + * @param expected_response_code which HTTP response code + * we expect from the exchange. + * @return the withdraw command to be executed by the interpreter. + */ +struct TALER_TESTING_Command +TALER_TESTING_cmd_withdraw_amount_reuse_key ( + const char *label, + const char *reserve_reference, + const char *amount, + const char *coin_ref, + unsigned int expected_response_code); + + /** * Create withdraw command, letting the caller specify the * amount by a denomination key. diff --git a/src/lib/exchange_api_melt.c b/src/lib/exchange_api_melt.c index 2c1e85d7b..989c6501a 100644 --- a/src/lib/exchange_api_melt.c +++ b/src/lib/exchange_api_melt.c @@ -75,6 +75,11 @@ struct TALER_EXCHANGE_MeltHandle */ struct MeltData *md; + /** + * Public key of the coin being melted. + */ + struct TALER_CoinSpendPublicKeyP coin_pub; + /** * @brief Public information about the coin's denomination key */ @@ -153,6 +158,48 @@ verify_melt_signature_ok (struct TALER_EXCHANGE_MeltHandle *mh, } +/** + * Verify that the signatures on the "409 CONFLICT" response from the + * exchange demonstrating customer denomination key differences + * resulting from coin private key reuse are valid. + * + * @param mh melt handle + * @param json json reply with the signature(s) and transaction history + * @return #GNUNET_OK if the signature(s) is valid, #GNUNET_SYSERR if not + */ +static int +verify_melt_signature_denom_conflict (struct TALER_EXCHANGE_MeltHandle *mh, + const json_t *json) + +{ + json_t *history; + struct TALER_Amount total; + struct GNUNET_HashCode h_denom_pub; + + memset (&h_denom_pub, + 0, + sizeof (h_denom_pub)); + history = json_object_get (json, + "history"); + if (GNUNET_OK != + TALER_EXCHANGE_verify_coin_history (&mh->dki, + mh->dki.value.currency, + &mh->coin_pub, + history, + &h_denom_pub, + &total)) + { + GNUNET_break_op (0); + return GNUNET_SYSERR; + } + if (0 != GNUNET_memcmp (&mh->dki.h_key, + &h_denom_pub)) + return GNUNET_OK; /* indeed, proof with different denomination key provided */ + /* invalid proof provided */ + return GNUNET_SYSERR; +} + + /** * Verify that the signatures on the "409 CONFLICT" response from the * exchange demonstrating customer double-spending are valid. @@ -162,8 +209,8 @@ verify_melt_signature_ok (struct TALER_EXCHANGE_MeltHandle *mh, * @return #GNUNET_OK if the signature(s) is valid, #GNUNET_SYSERR if not */ static int -verify_melt_signature_conflict (struct TALER_EXCHANGE_MeltHandle *mh, - const json_t *json) +verify_melt_signature_spend_conflict (struct TALER_EXCHANGE_MeltHandle *mh, + const json_t *json) { json_t *history; struct TALER_Amount original_value; @@ -329,15 +376,38 @@ handle_melt_finished (void *cls, hr.hint = TALER_JSON_get_error_hint (j); break; case MHD_HTTP_CONFLICT: - /* Double spending; check signatures on transaction history */ - if (GNUNET_OK != - verify_melt_signature_conflict (mh, - j)) + hr.ec = TALER_JSON_get_error_code (j); + switch (hr.ec) { + case TALER_EC_MELT_INSUFFICIENT_FUNDS: + /* Double spending; check signatures on transaction history */ + if (GNUNET_OK != + verify_melt_signature_spend_conflict (mh, + j)) + { + GNUNET_break_op (0); + hr.http_status = 0; + hr.ec = TALER_EC_MELT_INVALID_SIGNATURE_BY_EXCHANGE; + hr.hint = TALER_JSON_get_error_hint (j); + } + break; + case TALER_EC_COIN_CONFLICTING_DENOMINATION_KEY: + if (GNUNET_OK != + verify_melt_signature_denom_conflict (mh, + j)) + { + GNUNET_break_op (0); + hr.http_status = 0; + hr.ec = TALER_EC_MELT_INVALID_SIGNATURE_BY_EXCHANGE; + hr.hint = TALER_JSON_get_error_hint (j); + } + break; + default: GNUNET_break_op (0); hr.http_status = 0; hr.ec = TALER_EC_MELT_INVALID_SIGNATURE_BY_EXCHANGE; hr.hint = TALER_JSON_get_error_hint (j); + break; } break; case MHD_HTTP_FORBIDDEN: @@ -485,6 +555,7 @@ TALER_EXCHANGE_melt (struct TALER_EXCHANGE_Handle *exchange, /* and now we can at last begin the actual request handling */ mh = GNUNET_new (struct TALER_EXCHANGE_MeltHandle); mh->exchange = exchange; + mh->coin_pub = melt.coin_pub; mh->dki = *dki; mh->dki.key.rsa_public_key = NULL; /* lifetime not warranted, so better not copy the pointer */ diff --git a/src/testing/test_exchange_api.c b/src/testing/test_exchange_api.c index 410c1a492..75e3d480f 100644 --- a/src/testing/test_exchange_api.c +++ b/src/testing/test_exchange_api.c @@ -136,12 +136,12 @@ run (void *cls, * Do another transfer to the same reserve */ TALER_TESTING_cmd_admin_add_incoming_with_ref ("create-reserve-1.2", - "EUR:1", + "EUR:2.01", &bc.exchange_auth, bc.user42_payto, "create-reserve-1"), TALER_TESTING_cmd_check_bank_admin_transfer ("check-create-reserve-1.2", - "EUR:1", + "EUR:2.01", bc.user42_payto, bc.exchange_payto, "create-reserve-1.2"), @@ -153,6 +153,15 @@ run (void *cls, "create-reserve-1", "EUR:5", MHD_HTTP_OK), + /** + * Withdraw EUR:1 using the SAME private coin key as for the previous coin + * (in violation of the specification, to be detected on spending!). + */ + TALER_TESTING_cmd_withdraw_amount_reuse_key ("withdraw-coin-1x", + "create-reserve-1", + "EUR:1", + "withdraw-coin-1", + MHD_HTTP_OK), /** * Check the reserve is depleted. */ @@ -160,6 +169,13 @@ run (void *cls, "create-reserve-1", "EUR:0", MHD_HTTP_OK), + /* + * Try to overdraw. + */ + TALER_TESTING_cmd_withdraw_amount ("withdraw-coin-2", + "create-reserve-1", + "EUR:5", + MHD_HTTP_CONFLICT), TALER_TESTING_cmd_end () }; @@ -178,13 +194,14 @@ run (void *cls, TALER_TESTING_cmd_deposit_replay ("deposit-simple-replay", "deposit-simple", MHD_HTTP_OK), - /* - * Try to overdraw. - */ - TALER_TESTING_cmd_withdraw_amount ("withdraw-coin-2", - "create-reserve-1", - "EUR:5", - MHD_HTTP_CONFLICT), + TALER_TESTING_cmd_deposit ("deposit-reused-coin-key-failure", + "withdraw-coin-1x", + 0, + bc.user42_payto, + "{\"items\":[{\"name\":\"ice cream\",\"value\":1}]}", + GNUNET_TIME_UNIT_ZERO, + "EUR:1", + MHD_HTTP_CONFLICT), /** * Try to double spend using different wire details. */ @@ -225,6 +242,14 @@ run (void *cls, }; struct TALER_TESTING_Command refresh[] = { + /** + * Try to melt the coin that shared the private key with another + * coin (should fail). */ + TALER_TESTING_cmd_melt ("refresh-melt-reused-coin-key-failure", + "withdraw-coin-1x", + MHD_HTTP_CONFLICT, + NULL), + /* Fill reserve with EUR:5, 1ct is for fees. */ CMD_TRANSFER_TO_EXCHANGE ("refresh-create-reserve-1", "EUR:5.01"), diff --git a/src/testing/testing_api_cmd_withdraw.c b/src/testing/testing_api_cmd_withdraw.c index 5db97cbff..5b2ad26e4 100644 --- a/src/testing/testing_api_cmd_withdraw.c +++ b/src/testing/testing_api_cmd_withdraw.c @@ -59,6 +59,12 @@ struct WithdrawState */ const char *reserve_reference; + /** + * Reference to a withdraw or reveal operation from which we should + * re-use the private coin key, or NULL for regular withdrawal. + */ + const char *reuse_coin_key_ref; + /** * String describing the denomination value we should withdraw. * A corresponding denomination key must exist in the exchange's @@ -274,6 +280,50 @@ reserve_withdraw_cb (void *cls, } +/** + * Parser reference to a coin. + * + * @param coin_reference of format $LABEL['#' $INDEX]? + * @param[out] cref where we return a copy of $LABEL + * @param[out] idx where we set $INDEX + * @return #GNUNET_SYSERR if $INDEX is present but not numeric + */ +static int +parse_coin_reference (const char *coin_reference, + char **cref, + unsigned int *idx) +{ + const char *index; + + /* We allow command references of the form "$LABEL#$INDEX" or + just "$LABEL", which implies the index is 0. Figure out + which one it is. */ + index = strchr (coin_reference, '#'); + if (NULL == index) + { + *idx = 0; + *cref = GNUNET_strdup (coin_reference); + return GNUNET_OK; + } + *cref = GNUNET_strndup (coin_reference, + index - coin_reference); + if (1 != sscanf (index + 1, + "%u", + idx)) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Numeric index (not `%s') required after `#' in command reference of command in %s:%u\n", + index, + __FILE__, + __LINE__); + GNUNET_free (*cref); + *cref = NULL; + return GNUNET_SYSERR; + } + return GNUNET_OK; +} + + /** * Run the command. */ @@ -307,7 +357,32 @@ withdraw_run (void *cls, TALER_TESTING_interpreter_fail (is); return; } - TALER_planchet_setup_random (&ws->ps); + if (NULL == ws->reuse_coin_key_ref) + { + TALER_planchet_setup_random (&ws->ps); + } + else + { + const struct TALER_CoinSpendPrivateKeyP *coin_priv; + const struct TALER_TESTING_Command *cref; + char *cstr; + unsigned int index; + + GNUNET_assert (GNUNET_OK == + parse_coin_reference (ws->reuse_coin_key_ref, + &cstr, + &index)); + cref = TALER_TESTING_interpreter_lookup_command (is, + cstr); + GNUNET_assert (NULL != cref); + GNUNET_free (cstr); + GNUNET_assert (GNUNET_OK == + TALER_TESTING_get_trait_coin_priv (cref, + index, + &coin_priv)); + TALER_planchet_setup_random (&ws->ps); + ws->ps.coin_priv = *coin_priv; + } ws->is = is; if (NULL == ws->pk) { @@ -526,6 +601,44 @@ TALER_TESTING_cmd_withdraw_amount (const char *label, } +/** + * Create a withdraw command, letting the caller specify + * the desired amount as string and also re-using an existing + * coin private key in the process (violating the specification, + * which will result in an error when spending the coin!). + * + * @param label command label. + * @param reserve_reference command providing us with a reserve to withdraw from + * @param amount how much we withdraw. + * @param coin_ref reference to (withdraw/reveal) command of a coin + * from which we should re-use the private key + * @param expected_response_code which HTTP response code + * we expect from the exchange. + * @return the withdraw command to be executed by the interpreter. + */ +struct TALER_TESTING_Command +TALER_TESTING_cmd_withdraw_amount_reuse_key ( + const char *label, + const char *reserve_reference, + const char *amount, + const char *coin_ref, + unsigned int expected_response_code) +{ + struct TALER_TESTING_Command cmd; + + cmd = TALER_TESTING_cmd_withdraw_amount (label, + reserve_reference, + amount, + expected_response_code); + { + struct WithdrawState *ws = cmd.cls; + + ws->reuse_coin_key_ref = coin_ref; + } + return cmd; +} + + /** * Create withdraw command, letting the caller specify the * amount by a denomination key. -- cgit v1.2.3