merchant

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

commit 6e119570481107007fcde42b15f9b12acfabd5f9
parent de228e81ce6f2ac4aa2ac81f43c3f3951d378525
Author: Florian Dold <dold@taler.net>
Date:   Tue, 18 Aug 2026 21:29:05 +0200

merchant-httpd: make KYC response ordering deterministic

Diffstat:
Msrc/backend/meson.build | 22++++++++++++++++++++++
Msrc/backend/taler-merchant-httpd_get-private-kyc.c | 7+++++++
Asrc/backend/taler-merchant-httpd_kyc-order.c | 94+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/backend/taler-merchant-httpd_kyc-order.h | 34++++++++++++++++++++++++++++++++++
Asrc/backend/test_merchant_kyc_order.c | 132+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/include/taler/merchant/get-private-kyc.h | 3++-
6 files changed, 291 insertions(+), 1 deletion(-)

diff --git a/src/backend/meson.build b/src/backend/meson.build @@ -102,6 +102,7 @@ taler_merchant_httpd_SOURCES = [ 'taler-merchant-httpd_get-management-instances.c', 'taler-merchant-httpd_get-management-instances-INSTANCE.c', 'taler-merchant-httpd_get-private-kyc.c', + 'taler-merchant-httpd_kyc-order.c', 'taler-merchant-httpd_get-private-tokens.c', 'taler-merchant-httpd_get-private-pos.c', 'taler-merchant-httpd_get-private-products.c', @@ -355,3 +356,24 @@ if donau_dep.found() ) endif + + +test_merchant_kyc_order = executable( + 'test_merchant_kyc_order', + [ + 'test_merchant_kyc_order.c', + 'taler-merchant-httpd_kyc-order.c', + ], + dependencies: [ + talerjson_dep, + gnunetutil_dep, + json_dep, + ], + include_directories: [incdir, configuration_inc], + install: false, +) +test( + 'test_merchant_kyc_order', + test_merchant_kyc_order, + suite: ['backend'], +) diff --git a/src/backend/taler-merchant-httpd_get-private-kyc.c b/src/backend/taler-merchant-httpd_get-private-kyc.c @@ -25,6 +25,7 @@ #include "platform.h" #include "taler-merchant-httpd_exchanges.h" #include "taler-merchant-httpd_get-private-kyc.h" +#include "taler-merchant-httpd_kyc-order.h" #include "taler-merchant-httpd_helper.h" #include "taler-merchant-httpd_get-exchanges.h" #include <taler/taler_json_lib.h> @@ -1514,6 +1515,12 @@ resume_kyc_with_response (struct KycContext *kc) unsigned int response_code; struct MHD_Response *response; + /* The database returns KYC records in a stable order, but entries that + require exchange /keys data are appended when their asynchronous lookup + finishes. Sort only after all lookups are done so callback timing and + synthetic exchange-unreachable entries cannot affect the response or its + ETag. */ + TMH_kyc_data_sort (kc->kycs_data); can = TALER_JSON_canonicalize (kc->kycs_data); GNUNET_assert (GNUNET_YES == GNUNET_CRYPTO_hkdf_gnunet (&sh, diff --git a/src/backend/taler-merchant-httpd_kyc-order.c b/src/backend/taler-merchant-httpd_kyc-order.c @@ -0,0 +1,94 @@ +/* + 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 backend/taler-merchant-httpd_kyc-order.c + * @brief Deterministic ordering of KYC response data + */ +#include "platform.h" +#include <gnunet/gnunet_util_lib.h> +#include "taler-merchant-httpd_kyc-order.h" + + +/** + * Compare two entries in a KYC response. + * + * @param a pointer to the first JSON entry + * @param b pointer to the second JSON entry + * @return result of the comparison + */ +static int +compare_kyc_data (const void *a, + const void *b) +{ + const json_t *ja = *(const json_t * const *) a; + const json_t *jb = *(const json_t * const *) b; + const char *ha; + const char *hb; + const char *ea; + const char *eb; + int ret; + + ha = json_string_value (json_object_get (ja, + "h_wire")); + hb = json_string_value (json_object_get (jb, + "h_wire")); + ea = json_string_value (json_object_get (ja, + "exchange_url")); + eb = json_string_value (json_object_get (jb, + "exchange_url")); + GNUNET_assert (NULL != ha); + GNUNET_assert (NULL != hb); + GNUNET_assert (NULL != ea); + GNUNET_assert (NULL != eb); + ret = strcmp (ha, + hb); + if (0 != ret) + return ret; + return strcmp (ea, + eb); +} + + +void +TMH_kyc_data_sort (json_t *kyc_data) +{ + json_t **entries; + size_t len; + + GNUNET_assert (json_is_array (kyc_data)); + len = json_array_size (kyc_data); + if (2 > len) + return; + entries = GNUNET_new_array (len, + json_t *); + for (size_t i = 0; i < len; i++) + { + entries[i] = json_array_get (kyc_data, + i); + GNUNET_assert (NULL != entries[i]); + json_incref (entries[i]); + } + qsort (entries, + len, + sizeof (*entries), + &compare_kyc_data); + for (size_t i = 0; i < len; i++) + GNUNET_assert (0 == + json_array_set_new (kyc_data, + i, + entries[i])); + GNUNET_free (entries); +} diff --git a/src/backend/taler-merchant-httpd_kyc-order.h b/src/backend/taler-merchant-httpd_kyc-order.h @@ -0,0 +1,34 @@ +/* + 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 backend/taler-merchant-httpd_kyc-order.h + * @brief Deterministic ordering of KYC response data + */ +#ifndef TALER_MERCHANT_HTTPD_KYC_ORDER_H +#define TALER_MERCHANT_HTTPD_KYC_ORDER_H + +#include <jansson.h> + + +/** + * Sort KYC response entries by account hash and exchange URL. + * + * @param[in,out] kyc_data array of KYC response entries to sort + */ +void +TMH_kyc_data_sort (json_t *kyc_data); + +#endif diff --git a/src/backend/test_merchant_kyc_order.c b/src/backend/test_merchant_kyc_order.c @@ -0,0 +1,132 @@ +/* + 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 backend/test_merchant_kyc_order.c + * @brief Test deterministic ordering of KYC response data + */ +#include "platform.h" +#include <gnunet/gnunet_util_lib.h> +#include <taler/taler_json_lib.h> +#include "taler-merchant-httpd_kyc-order.h" + + +/** + * Check one entry in a sorted KYC response. + * + * @param data KYC response array + * @param index index to check + * @param h_wire expected account hash + * @param exchange_url expected exchange URL + */ +static void +check_entry (const json_t *data, + size_t index, + const char *h_wire, + const char *exchange_url) +{ + const json_t *entry = json_array_get (data, + index); + + GNUNET_assert (0 == + strcmp (h_wire, + json_string_value ( + json_object_get (entry, + "h_wire")))); + GNUNET_assert (0 == + strcmp (exchange_url, + json_string_value ( + json_object_get (entry, + "exchange_url")))); +} + + +int +main (int argc, + char *const argv[]) +{ + json_t *first; + json_t *second; + char *first_canonical; + char *second_canonical; + struct GNUNET_ShortHashCode first_etag; + struct GNUNET_ShortHashCode second_etag; + + (void) argc; + (void) argv; + first = json_pack ( + "[{s:s,s:s},{s:s,s:s},{s:s,s:s}]", + "h_wire", "BBBB", + "exchange_url", "https://b.example/", + "h_wire", "AAAA", + "exchange_url", "https://z.example/", + "h_wire", "AAAA", + "exchange_url", "https://a.example/"); + second = json_pack ( + "[{s:s,s:s},{s:s,s:s},{s:s,s:s}]", + "h_wire", "AAAA", + "exchange_url", "https://a.example/", + "h_wire", "BBBB", + "exchange_url", "https://b.example/", + "h_wire", "AAAA", + "exchange_url", "https://z.example/"); + GNUNET_assert (NULL != first); + GNUNET_assert (NULL != second); + + TMH_kyc_data_sort (first); + TMH_kyc_data_sort (second); + check_entry (first, + 0, + "AAAA", + "https://a.example/"); + check_entry (first, + 1, + "AAAA", + "https://z.example/"); + check_entry (first, + 2, + "BBBB", + "https://b.example/"); + GNUNET_assert (json_equal (first, + second)); + first_canonical = TALER_JSON_canonicalize (first); + second_canonical = TALER_JSON_canonicalize (second); + GNUNET_assert (0 == + strcmp (first_canonical, + second_canonical)); + GNUNET_assert (GNUNET_YES == + GNUNET_CRYPTO_hkdf_gnunet (&first_etag, + sizeof (first_etag), + "KYC-SALT", + strlen ("KYC-SALT"), + first_canonical, + strlen (first_canonical))); + GNUNET_assert (GNUNET_YES == + GNUNET_CRYPTO_hkdf_gnunet (&second_etag, + sizeof (second_etag), + "KYC-SALT", + strlen ("KYC-SALT"), + second_canonical, + strlen (second_canonical))); + GNUNET_assert (0 == + GNUNET_memcmp (&first_etag, + &second_etag)); + + GNUNET_free (first_canonical); + GNUNET_free (second_canonical); + json_decref (first); + json_decref (second); + return 0; +} diff --git a/src/include/taler/merchant/get-private-kyc.h b/src/include/taler/merchant/get-private-kyc.h @@ -471,7 +471,8 @@ struct TALER_MERCHANT_GetPrivateKycResponse unsigned int kycs_length; /** - * Array of KYC redirect details for accounts requiring KYC. + * Array of KYC redirect details for accounts requiring KYC, + * sorted by @c h_wire and then by exchange URL. */ const struct TALER_MERCHANT_GetPrivateKycRedirectDetail *kycs;