taler-typescript-core

Wallet core logic and WebUIs for various components
Log | Files | Refs | Submodules | README | LICENSE

commit 9a5461eff67ed94c1e9beab060d38bc52e6652f6
parent 44988c85c2fe24478bdc5b48f13dba18137f4f47
Author: Florian Dold <dold@taler.net>
Date:   Thu, 24 Sep 2026 00:25:02 +0200

harness: check that a forged challenger error redirect cannot fail KYC

test-kyc-challenger now uses the state the exchange issued, as the exchange
rejects the bare account hash.

Issue: https://bugs.taler.net/n/11740

Diffstat:
Mpackages/taler-harness/src/integrationtests/test-kyc-challenger.ts | 9+++++++--
Mpackages/taler-harness/src/integrationtests/test-tops-aml-challenger-failure.ts | 53++++++++++++++++++++++++++++++++++++-----------------
2 files changed, 43 insertions(+), 19 deletions(-)

diff --git a/packages/taler-harness/src/integrationtests/test-kyc-challenger.ts b/packages/taler-harness/src/integrationtests/test-kyc-challenger.ts @@ -346,12 +346,17 @@ export async function runKycChallengerTest(t: GlobalTestState) { ); logger.info(`kyc-start resp status: ${startResp.status}`); - logger.info(j2s(await startResp.json())); + const startJson = await startResp.json(); + logger.info(j2s(startJson)); + // The provider hands back the state the exchange put into the + // authorization URL; the exchange rejects any other. + const state = new URL(startJson.redirect_url).searchParams.get("state"); + t.assertTrue(!!state); // We need to "visit" the KYC proof URL at least once to trigger the exchange // asking for the KYC status. const proofUrl = new URL(`kyc-proof/myprov`, exchange.baseUrl); - proofUrl.searchParams.set("state", paytoHash); + proofUrl.searchParams.set("state", state); proofUrl.searchParams.set("code", "code_is_ok"); // Check the exchange's completion redirect without visiting an external // website, whose availability is unrelated to this local KYC flow. diff --git a/packages/taler-harness/src/integrationtests/test-tops-aml-challenger-failure.ts b/packages/taler-harness/src/integrationtests/test-tops-aml-challenger-failure.ts @@ -49,6 +49,10 @@ import { * (bug 11740). The exchange must then finish the KYC process as failed * instead of treating the answer as an unexpected provider reply and * leaving the process pending. + * + * As such a redirect is not authenticated, the exchange must only trust + * it if it carries the unguessable state the exchange issued for the + * process; anyone knowing just the account must not be able to fail it. */ export async function runTopsAmlChallengerFailureTest(t: GlobalTestState) { const { @@ -153,6 +157,37 @@ export async function runTopsAmlChallengerFailureTest(t: GlobalTestState) { const errorRedirectUrl = authorizeJson.error_redirect_url; t.assertTrue(typeof errorRedirectUrl === "string"); + const hPayto = `decode('${Buffer.from(decodeCrock(merchantPaytoHash)).toString("hex")}', 'hex')`; + const getProcessState = async () => { + const processState = await runCommand(t, "kyc-process-state", "psql", [ + commonDb.connStr, + "-X", + "-qAt", + "-v", + "ON_ERROR_STOP=1", + "-c", + `SELECT finished, error_code + FROM exchange.legitimization_processes + WHERE h_payto=${hPayto} + AND provider_name='sms-challenger' + ORDER BY legitimization_process_serial_id DESC + LIMIT 1`, + ]); + console.log(`KYC process state: ${processState}`); + return processState.trim(); + }; + + // Someone who only knows the account forges the error redirect. + const forgedUrl = new URL(errorRedirectUrl); + t.assertTrue(forgedUrl.searchParams.get("state") !== merchantPaytoHash); + forgedUrl.searchParams.set("state", merchantPaytoHash); + const forgedResp = await harnessHttpLib.fetch(forgedUrl.href, { + redirect: "manual", + }); + console.log("forged proof status:", forgedResp.status); + t.assertDeepEqual(forgedResp.status, 403); + t.assertDeepEqual((await getProcessState()).split("|")[0], "f"); + // The user used up all their attempts; challenger sends them back. const proofResp = await harnessHttpLib.fetch(errorRedirectUrl, { redirect: "manual", @@ -162,24 +197,8 @@ export async function runTopsAmlChallengerFailureTest(t: GlobalTestState) { // unexpected provider reply. t.assertDeepEqual(proofResp.status, 403); - const hPayto = `decode('${Buffer.from(decodeCrock(merchantPaytoHash)).toString("hex")}', 'hex')`; - const processState = await runCommand(t, "kyc-process-state", "psql", [ - commonDb.connStr, - "-X", - "-qAt", - "-v", - "ON_ERROR_STOP=1", - "-c", - `SELECT finished, error_code - FROM exchange.legitimization_processes - WHERE h_payto=${hPayto} - AND provider_name='sms-challenger' - ORDER BY legitimization_process_serial_id DESC - LIMIT 1`, - ]); - console.log(`KYC process state: ${processState}`); t.assertDeepEqual( - processState.trim(), + await getProcessState(), `t|${TalerErrorCode.EXCHANGE_GENERIC_KYC_FAILED}`, ); }