merchant

Merchant backend to process payments, run by merchants
Log | Files | Refs | Submodules | README | LICENSE

commit a075795020d665fd12cf99e96f08969e6df9406a
parent 534edf2d0fc0e2a03736b9251703bad2ffaf14f7
Author: Christian Grothoff <christian@grothoff.org>
Date:   Wed, 18 May 2022 18:34:20 +0200

Merge branch 'master' of git+ssh://git.taler.net/merchant

Diffstat:
Msrc/backend/taler-merchant-httpd.c | 1+
Msrc/backend/taler-merchant-httpd.h | 7+++++++
Msrc/backend/taler-merchant-httpd_get-orders-ID.c | 108+++++++++++++++++++++++++++++++++++++++++++++++++++++--------------------------
Msrc/backend/taler-merchant-httpd_post-orders-ID-claim.c | 14++++++--------
Msrc/backend/taler-merchant-httpd_post-orders-ID-paid.c | 18+++++++-----------
Msrc/backend/taler-merchant-httpd_post-orders-ID-pay.c | 85+++++++++++++++++++++++++++++++++++++++++++------------------------------------
Msrc/backend/taler-merchant-httpd_private-get-orders-ID.c | 4+++-
Msrc/backend/taler-merchant-httpd_private-get-orders.c | 3++-
Msrc/backend/taler-merchant-httpd_private-post-instances-ID-auth.c | 18++++++++++++------
Msrc/lib/merchant_api_post_order_pay.c | 12+++---------
Msrc/testing/test_kyc_api.conf | 3++-
11 files changed, 162 insertions(+), 111 deletions(-)

diff --git a/src/backend/taler-merchant-httpd.c b/src/backend/taler-merchant-httpd.c @@ -1186,6 +1186,7 @@ url_handler (void *cls, TMH_compute_auth (TMH_default_auth, &hc->instance->auth.auth_salt, &hc->instance->auth.auth_hash); + hc->instance->auth_override = true; GNUNET_free (TMH_default_auth); } } diff --git a/src/backend/taler-merchant-httpd.h b/src/backend/taler-merchant-httpd.h @@ -163,6 +163,13 @@ struct TMH_MerchantInstance * True if this instance was deleted (but not yet purged). */ bool deleted; + + /** + * The authentication settings for this instance + * were changed via the command-line. Do not check + * against the DB value when updating the auth token. + */ + bool auth_override; }; diff --git a/src/backend/taler-merchant-httpd_get-orders-ID.c b/src/backend/taler-merchant-httpd_get-orders-ID.c @@ -125,6 +125,12 @@ struct GetOrderData struct TALER_Amount refund_amount; /** + * Total refunds already collected. + * if @e refunded is set to true. + */ + struct TALER_Amount refund_taken; + + /** * Return code: #TALER_EC_NONE if successful. */ enum TALER_ErrorCode ec; @@ -154,8 +160,9 @@ struct GetOrderData /** * Set to true if a refund is still available for the * wallet for this payment. + * @deprecated: true if refund_taken < refund_amount */ - bool refund_available; + bool refund_pending; /** * Set to true if the client requested HTML, otherwise we generate JSON. @@ -756,16 +763,18 @@ process_refunds_cb (void *cls, TALER_amount2s (refund_amount), TALER_B2S (coin_pub), reason); - god->refund_available |= pending; - if (god->refunded) - { + god->refund_pending |= pending; + if (!pending) + { GNUNET_assert (0 <= - TALER_amount_add (&god->refund_amount, - &god->refund_amount, - refund_amount)); - return; + TALER_amount_add (&god->refund_taken, + &god->refund_taken, + refund_amount)); } - god->refund_amount = *refund_amount; + GNUNET_assert (0 <= + TALER_amount_add (&god->refund_amount, + &god->refund_amount, + refund_amount)); god->refunded = true; } @@ -1052,33 +1061,53 @@ TMH_get_orders_ID (const struct TMH_RequestHandler *rh, /* Check if client provided the right hash code of the contract terms */ if (NULL != god->contract_terms) { - struct TALER_PrivateContractHashP h; - contract_available = true; - if (GNUNET_OK != - TALER_JSON_contract_hash (god->contract_terms, - &h)) + + if (GNUNET_YES == GNUNET_is_zero (&god->h_contract_terms)) { - GNUNET_break (0); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_FAILED_COMPUTE_JSON_HASH, - "contract terms"); + + if (GNUNET_OK != + TALER_JSON_contract_hash (god->contract_terms, + &god->h_contract_terms)) + { + GNUNET_break (0); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_FAILED_COMPUTE_JSON_HASH, + "contract terms"); + } + } - contract_match = (0 == - GNUNET_memcmp (&h, - &god->h_contract_terms)); - if ( (GNUNET_NO == - GNUNET_is_zero (&god->h_contract_terms)) && - (! contract_match) ) + else { - GNUNET_break_op (0); - return TALER_MHD_reply_with_error ( - connection, - MHD_HTTP_FORBIDDEN, - TALER_EC_MERCHANT_GENERIC_CONTRACT_HASH_DOES_NOT_MATCH_ORDER, - NULL); + + struct TALER_PrivateContractHashP h; + + if (GNUNET_OK != + TALER_JSON_contract_hash (god->contract_terms, + &h)) + { + GNUNET_break (0); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_FAILED_COMPUTE_JSON_HASH, + "contract terms"); + } + contract_match = (0 == + GNUNET_memcmp (&h, + &god->h_contract_terms)); + if ( !contract_match ) + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_FORBIDDEN, + TALER_EC_MERCHANT_GENERIC_CONTRACT_HASH_DOES_NOT_MATCH_ORDER, + NULL); + } + } + } if (contract_available) @@ -1341,6 +1370,9 @@ TMH_get_orders_ID (const struct TMH_RequestHandler *rh, GNUNET_assert (GNUNET_OK == TALER_amount_set_zero (TMH_currency, &god->refund_amount)); + GNUNET_assert (GNUNET_OK == + TALER_amount_set_zero (TMH_currency, + &god->refund_taken)); qs = TMH_db->lookup_refunds_detailed (TMH_db->cls, hc->instance->settings.id, &god->h_contract_terms, @@ -1360,7 +1392,7 @@ TMH_get_orders_ID (const struct TMH_RequestHandler *rh, (1 != TALER_amount_cmp (&god->refund_amount, &god->sc.refund_expected)) )) || ( (god->sc.awaiting_refund_obtained) && - (god->refund_available) ) ) + (god->refund_pending) ) ) { /* Client is waiting for a refund larger than what we have, suspend until timeout */ @@ -1387,7 +1419,7 @@ TMH_get_orders_ID (const struct TMH_RequestHandler *rh, { enum GNUNET_GenericReturnValue res; - if (god->refund_available) + if (god->refund_pending) { char *qr; char *uri; @@ -1421,6 +1453,8 @@ TMH_get_orders_ID (const struct TMH_RequestHandler *rh, get_order_summary (god)), TALER_JSON_pack_amount ("refund_amount", &god->refund_amount), + TALER_JSON_pack_amount ("refund_taken", + &god->refund_taken), GNUNET_JSON_pack_string ("taler_refund_uri", uri), GNUNET_JSON_pack_string ("taler_refund_qrcode_svg", @@ -1446,7 +1480,9 @@ TMH_get_orders_ID (const struct TMH_RequestHandler *rh, GNUNET_JSON_pack_string ("order_summary", get_order_summary (god)), TALER_JSON_pack_amount ("refund_amount", - &god->refund_amount)); + &god->refund_amount), + TALER_JSON_pack_amount ("refund_taken", + &god->refund_taken)); res = TMH_return_from_template (god->sc.con, MHD_HTTP_OK, "show_order_details", @@ -1471,7 +1507,9 @@ TMH_get_orders_ID (const struct TMH_RequestHandler *rh, GNUNET_JSON_pack_bool ("refunded", god->refunded), GNUNET_JSON_pack_bool ("refund_pending", - god->refund_available), + god->refund_pending), + TALER_JSON_pack_amount ("refund_taken", + &god->refund_taken), TALER_JSON_pack_amount ("refund_amount", &god->refund_amount)); } diff --git a/src/backend/taler-merchant-httpd_post-orders-ID-claim.c b/src/backend/taler-merchant-httpd_post-orders-ID-claim.c @@ -265,11 +265,9 @@ TMH_post_orders_ID_claim (const struct TMH_RequestHandler *rh, /* create contract signature */ { + struct TALER_PrivateContractHashP hash; struct GNUNET_CRYPTO_EddsaSignature merchant_sig; - struct TALER_ProposalDataPS pdps = { - .purpose.purpose = htonl (TALER_SIGNATURE_MERCHANT_CONTRACT), - .purpose.size = htonl (sizeof (pdps)) - }; + /** * Hash of the JSON contract in UTF-8 including 0-termination, * using JSON_COMPACT | JSON_SORT_KEYS @@ -277,7 +275,7 @@ TMH_post_orders_ID_claim (const struct TMH_RequestHandler *rh, if (GNUNET_OK != TALER_JSON_contract_hash (contract_terms, - &pdps.hash)) + &hash)) { GNUNET_break (0); json_decref (contract_terms); @@ -287,9 +285,9 @@ TMH_post_orders_ID_claim (const struct TMH_RequestHandler *rh, NULL); } - GNUNET_CRYPTO_eddsa_sign (&hc->instance->merchant_priv.eddsa_priv, - &pdps, - &merchant_sig); + TALER_merchant_contract_sign (&hash, + &hc->instance->merchant_priv, + &merchant_sig); return TALER_MHD_REPLY_JSON_PACK ( connection, MHD_HTTP_OK, diff --git a/src/backend/taler-merchant-httpd_post-orders-ID-paid.c b/src/backend/taler-merchant-httpd_post-orders-ID-paid.c @@ -67,13 +67,10 @@ TMH_post_orders_ID_paid (const struct TMH_RequestHandler *rh, struct MHD_Connection *connection, struct TMH_HandlerContext *hc) { - struct TALER_PaymentResponsePS pr = { - .purpose.purpose = htonl (TALER_SIGNATURE_MERCHANT_PAYMENT_OK), - .purpose.size = htonl (sizeof (pr)) - }; const char *order_id = hc->infix; struct TALER_MerchantSignatureP merchant_sig; const char *session_id; + struct TALER_PrivateContractHashP hct; json_t *contract_terms; const char *fulfillment_url; enum GNUNET_DB_QueryStatus qs; @@ -83,7 +80,7 @@ TMH_post_orders_ID_paid (const struct TMH_RequestHandler *rh, GNUNET_JSON_spec_fixed_auto ("sig", &merchant_sig), GNUNET_JSON_spec_fixed_auto ("h_contract", - &pr.h_contract_terms), + &hct), GNUNET_JSON_spec_string ("session_id", &session_id), GNUNET_JSON_spec_end () @@ -104,10 +101,9 @@ TMH_post_orders_ID_paid (const struct TMH_RequestHandler *rh, if (GNUNET_OK != - GNUNET_CRYPTO_eddsa_verify (TALER_SIGNATURE_MERCHANT_PAYMENT_OK, - &pr, - &merchant_sig.eddsa_sig, - &hc->instance->merchant_pub.eddsa_pub)) + TALER_merchant_pay_verify (&hct, + &hc->instance->merchant_pub, + &merchant_sig)) { GNUNET_break_op (0); return TALER_MHD_reply_with_error (connection, @@ -164,7 +160,7 @@ TMH_post_orders_ID_paid (const struct TMH_RequestHandler *rh, TALER_EC_GENERIC_FAILED_COMPUTE_JSON_HASH, NULL); } - if (0 != GNUNET_memcmp (&pr.h_contract_terms, + if (0 != GNUNET_memcmp (&hct, &h_contract_terms)) { json_decref (contract_terms); @@ -186,7 +182,7 @@ TMH_post_orders_ID_paid (const struct TMH_RequestHandler *rh, fulfillment_url); qs = TMH_db->mark_contract_paid (TMH_db->cls, hc->instance->settings.id, - &pr.h_contract_terms, + &hct, session_id); /* If the order was paid already, we get qs == 0. */ if (0 > qs) diff --git a/src/backend/taler-merchant-httpd_post-orders-ID-pay.c b/src/backend/taler-merchant-httpd_post-orders-ID-pay.c @@ -124,9 +124,10 @@ struct DepositConfirmation /** * If a minimum age was required (i. e. pc->minimum_age is large enough), * this is the signature of the minimum age (as a single uint8_t), using the - * private key to the corresponding age group. Might be NULL. + * private key to the corresponding age group. Might be all zeroes for no + * age attestation. */ - struct TALER_AgeAttestation *minimum_age_sig; + struct TALER_AgeAttestation minimum_age_sig; /* If a minimum age was required (i. e. pc->minimum_age is large enought), * this is the age commitment (i. e. age mask and vector of EdDSA public @@ -1193,7 +1194,7 @@ process_pay_with_exchange (void *cls, } dc->age_commitment->mask = denom_details->key.age_mask; - if (dc->age_commitment->num != + if ((dc->age_commitment->num + 1) != __builtin_popcount (dc->age_commitment->mask.bits)) { code = @@ -1205,7 +1206,7 @@ process_pay_with_exchange (void *cls, TALER_age_commitment_verify ( dc->age_commitment, pc->minimum_age, - dc->minimum_age_sig)) + &dc->minimum_age_sig)) code = TALER_EC_MERCHANT_POST_ORDERS_ID_PAY_AGE_VERIFICATION_FAILED; AGE_FAIL: @@ -1927,17 +1928,9 @@ execute_pay_transaction (struct PayContext *pc) /* Sign on our end (as the payment did go through, even if it may have been refunded already) */ - { - struct TALER_PaymentResponsePS mr = { - .purpose.purpose = htonl (TALER_SIGNATURE_MERCHANT_PAYMENT_OK), - .purpose.size = htonl (sizeof (mr)), - .h_contract_terms = pc->h_contract_terms - }; - - GNUNET_CRYPTO_eddsa_sign (&pc->hc->instance->merchant_priv.eddsa_priv, - &mr, - &sig); - } + TALER_merchant_pay_sign (&pc->h_contract_terms, + &pc->hc->instance->merchant_priv, + &sig); /* Build the response */ resume_pay_with_response ( @@ -2042,7 +2035,6 @@ parse_pay (struct MHD_Connection *connection, { struct DepositConfirmation *dc = &pc->dc[coins_index]; const char *exchange_url; - struct TALER_AgeAttestation minimum_age_sig = {0}; json_t *age_commitment = NULL; struct GNUNET_JSON_Specification ispec[] = { GNUNET_JSON_spec_fixed_auto ("coin_sig", @@ -2060,7 +2052,7 @@ parse_pay (struct MHD_Connection *connection, &exchange_url), GNUNET_JSON_spec_mark_optional ( GNUNET_JSON_spec_fixed_auto ("minimum_age_sig", - &minimum_age_sig), + &dc->minimum_age_sig), NULL), GNUNET_JSON_spec_mark_optional ( GNUNET_JSON_spec_json ("age_commitment", @@ -2092,8 +2084,8 @@ parse_pay (struct MHD_Connection *connection, MHD_HTTP_BAD_REQUEST, TALER_EC_GENERIC_PARAMETER_MALFORMED, "duplicate coin in list")) - ? GNUNET_NO - : GNUNET_SYSERR; + ? GNUNET_NO + : GNUNET_SYSERR; } } @@ -2107,26 +2099,32 @@ parse_pay (struct MHD_Connection *connection, { GNUNET_break_op (0); GNUNET_JSON_parse_free (spec); - return TALER_MHD_reply_with_error (connection, + return (MHD_YES == TALER_MHD_reply_with_error (connection, MHD_HTTP_CONFLICT, TALER_EC_GENERIC_CURRENCY_MISMATCH, - TMH_currency); + TMH_currency)) + ? GNUNET_NO + : GNUNET_SYSERR; } { bool has_commitment = (NULL != age_commitment) && json_is_array (age_commitment); - bool has_sig = ! GNUNET_is_zero_ (&minimum_age_sig, - sizeof(minimum_age_sig)); + bool has_sig = ! GNUNET_is_zero_ (&dc->minimum_age_sig, + sizeof(dc->minimum_age_sig)); if (has_sig != has_commitment) { GNUNET_break_op (0); GNUNET_JSON_parse_free (spec); - return TALER_MHD_reply_with_error ( - connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PARAMETER_MALFORMED, - "inconsistency: 'mininum_age_sig' vs. 'age_commitment'"); + return (MHD_YES == + TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "inconsistency: 'mininum_age_sig' vs. 'age_commitment'") + ) + ? GNUNET_NO + : GNUNET_SYSERR; } /* Parse the AgeCommitment, i. e. the public keys */ @@ -2138,14 +2136,18 @@ parse_pay (struct MHD_Connection *connection, /* Sanity check the amount of AgeCommitment's public keys. The * actual check will be performed once we now the denominations. */ - if (32 >= num) + if (32 <= num) { GNUNET_break_op (0); GNUNET_JSON_parse_free (spec); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PARAMETER_MALFORMED, - "'age_commitment' too large"); + return (MHD_YES == + TALER_MHD_reply_with_error (connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "'age_commitment' too large" + )) + ? GNUNET_NO + : GNUNET_SYSERR; } dc->age_commitment = GNUNET_new (struct TALER_AgeCommitment); @@ -2172,10 +2174,14 @@ parse_pay (struct MHD_Connection *connection, { GNUNET_break_op (0); GNUNET_JSON_parse_free (spec); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PARAMETER_MALFORMED, - "age_commitment"); + return (MHD_YES == + TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "age_commitment")) + ? GNUNET_NO + : GNUNET_SYSERR; } } } @@ -2288,8 +2294,8 @@ parse_pay (struct MHD_Connection *connection, GNUNET_JSON_spec_fixed_auto ("h_wire", &pc->h_wire), GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_fixed_auto ("minimum_age", - &pc->minimum_age), + GNUNET_JSON_spec_uint32 ("minimum_age", + &pc->minimum_age), NULL), GNUNET_JSON_spec_end () }; @@ -2451,6 +2457,7 @@ TMH_post_orders_ID_pay (const struct TMH_RequestHandler *rh, = GNUNET_SCHEDULER_add_delayed (get_pay_timeout (pc->coins_cnt), &handle_pay_timeout, pc); + GNUNET_assert (NULL != pc->wm); execute_pay_transaction (pc); return MHD_YES; } diff --git a/src/backend/taler-merchant-httpd_private-get-orders-ID.c b/src/backend/taler-merchant-httpd_private-get-orders-ID.c @@ -776,6 +776,8 @@ process_refunds_cb (void *cls, GNUNET_JSON_PACK ( TALER_JSON_pack_amount ("amount", refund_amount), + GNUNET_JSON_pack_bool ("pending", + pending), GNUNET_JSON_pack_timestamp ("timestamp", timestamp), GNUNET_JSON_pack_string ("reason", @@ -801,7 +803,7 @@ process_refunds_cb (void *cls, &gorc->refund_amount, refund_amount)); gorc->refunded = true; - gorc->refund_pending = pending; + gorc->refund_pending |= pending; } diff --git a/src/backend/taler-merchant-httpd_private-get-orders.c b/src/backend/taler-merchant-httpd_private-get-orders.c @@ -323,7 +323,8 @@ add_order (void *cls, &contract_terms, &os, NULL); - GNUNET_break (os == order_serial); + if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT == qs) + GNUNET_break (os == order_serial); } if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) { diff --git a/src/backend/taler-merchant-httpd_private-post-instances-ID-auth.c b/src/backend/taler-merchant-httpd_private-post-instances-ID-auth.c @@ -102,6 +102,7 @@ post_instances_ID_auth (struct TMH_MerchantInstance *mi, to the authentication. */ { struct TALER_MERCHANTDB_InstanceAuthSettings db_ias; + qs = TMH_db->lookup_instance_auth (TMH_db->cls, mi->settings.id, &db_ias); @@ -112,8 +113,8 @@ post_instances_ID_auth (struct TMH_MerchantInstance *mi, /* Instance got purged. */ TMH_db->rollback (TMH_db->cls); return TALER_MHD_reply_with_error (connection, - MHD_HTTP_UNAUTHORIZED, - TALER_EC_GENERIC_DB_COMMIT_FAILED, + MHD_HTTP_NOT_FOUND, + TALER_EC_MERCHANT_GENERIC_INSTANCE_UNKNOWN, NULL); case GNUNET_DB_STATUS_SOFT_ERROR: TMH_db->rollback (TMH_db->cls); @@ -129,12 +130,16 @@ post_instances_ID_auth (struct TMH_MerchantInstance *mi, break; } - if (GNUNET_OK != - TMH_check_auth (hc->auth_token, - &db_ias.auth_salt, - &db_ias.auth_hash)) + if ( (NULL == TMH_default_auth) && + (! mi->auth_override) && + (GNUNET_OK != + TMH_check_auth (hc->auth_token, + &db_ias.auth_salt, + &db_ias.auth_hash)) ) { TMH_db->rollback (TMH_db->cls); + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Refusing auth change: old token does not match\n"); return TALER_MHD_reply_with_error (connection, MHD_HTTP_UNAUTHORIZED, TALER_EC_MERCHANT_GENERIC_UNAUTHORIZED, @@ -175,6 +180,7 @@ retry: /* Finally, also update our running process */ mi->auth = ias; } + mi->auth_override = false; if (0 == strcmp (mi->settings.id, "default")) { diff --git a/src/lib/merchant_api_post_order_pay.c b/src/lib/merchant_api_post_order_pay.c @@ -265,11 +265,6 @@ handle_pay_finished (void *cls, if (oph->am_wallet) { /* Here we can (and should) verify the merchant's signature */ - struct TALER_PaymentResponsePS pr = { - .purpose.purpose = htonl (TALER_SIGNATURE_MERCHANT_PAYMENT_OK), - .purpose.size = htonl (sizeof (pr)), - .h_contract_terms = oph->h_contract_terms - }; struct GNUNET_JSON_Specification spec[] = { GNUNET_JSON_spec_fixed_auto ("sig", &merchant_sig), @@ -289,10 +284,9 @@ handle_pay_finished (void *cls, } if (GNUNET_OK != - GNUNET_CRYPTO_eddsa_verify (TALER_SIGNATURE_MERCHANT_PAYMENT_OK, - &pr, - &merchant_sig.eddsa_sig, - &oph->merchant_pub.eddsa_pub)) + TALER_merchant_pay_verify (&oph->h_contract_terms, + &oph->merchant_pub, + &merchant_sig)) { GNUNET_break_op (0); hr.ec = TALER_EC_GENERIC_INVALID_RESPONSE; diff --git a/src/testing/test_kyc_api.conf b/src/testing/test_kyc_api.conf @@ -113,7 +113,8 @@ KYC_WITHDRAW_LIMIT = EUR:20 [exchange-kyc-oauth2] -KYC_OAUTH2_URL = http://localhost:6666/oauth/v2/login +KYC_OAUTH2_AUTH_URL = http://localhost:6666/oauth/v2/token +KYC_OAUTH2_LOGIN_URL = http://localhost:6666/oauth/v2/login KYC_INFO_URL = http://localhost:6666/api/user/me KYC_OAUTH2_CLIENT_ID = taler-exchange KYC_OAUTH2_CLIENT_SECRET = exchange-secret