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:
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;