taler-typescript-core

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

commit 1b6e8d103ff34154ba98af15e5ad3873156ae21c
parent 68290f749950cba50d55b3def5ddea240ac4bb99
Author: Florian Dold <dold@taler.net>
Date:   Thu, 20 Aug 2026 19:06:49 +0200

wallet-core: test exchange response signing-key gates

Diffstat:
Mpackages/taler-wallet-core/src/exchange-signatures.test.ts | 125++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
1 file changed, 124 insertions(+), 1 deletion(-)

diff --git a/packages/taler-wallet-core/src/exchange-signatures.test.ts b/packages/taler-wallet-core/src/exchange-signatures.test.ts @@ -16,13 +16,19 @@ import { AbsoluteTime, + AmountString, + Duration, EddsaPublicKeyString, TalerProtocolTimestamp, } from "@gnu-taler/taler-util"; import assert from "node:assert"; import { test } from "node:test"; import { timestampProtocolToDb, WalletExchangeSignkeys } from "./db-common.js"; -import { exchangeSigningKeyIsUsable } from "./exchange-signatures.js"; +import { + exchangeSigningKeyIsUsable, + requireValidDirectExchangeRefundConfirmation, + requireValidExchangePurseStatus, +} from "./exchange-signatures.js"; const t = (seconds: number) => TalerProtocolTimestamp.fromSeconds(seconds); @@ -56,3 +62,120 @@ test("exchange response keys must match and cover the signing time", () => { false, ); }); + +function verificationContext(args: { + cryptoValid: boolean; + knownPub: string; +}): any { + const now = AbsoluteTime.now(); + const key: WalletExchangeSignkeys = { + exchangeDetailsRowId: 1, + signkeyPub: args.knownPub as EddsaPublicKeyString, + stampStart: timestampProtocolToDb( + AbsoluteTime.toProtocolTimestamp( + AbsoluteTime.subtractDuraction(now, Duration.fromSpec({ hours: 1 })), + ), + ), + stampExpire: timestampProtocolToDb( + AbsoluteTime.toProtocolTimestamp( + AbsoluteTime.addDuration(now, Duration.fromSpec({ hours: 1 })), + ), + ), + stampEnd: timestampProtocolToDb( + AbsoluteTime.toProtocolTimestamp( + AbsoluteTime.addDuration(now, Duration.fromSpec({ hours: 2 })), + ), + ), + masterSig: "master-signature", + }; + return { + cryptoApi: { + async isValidPurseStatus() { + return { valid: args.cryptoValid }; + }, + async isValidRefundConfirmation() { + return { valid: args.cryptoValid }; + }, + }, + async runWalletDbTx(fn: (tx: any) => Promise<unknown>) { + return await fn({ + async getExchangeDetails() { + return { rowId: 1 }; + }, + async getExchangeSignKeysByDetailsRowId() { + return [key]; + }, + }); + }, + }; +} + +test("purse status requires a valid signature from a known exchange key", async () => { + const status = { + balance: "TESTKUDOS:1", + deposit_timestamp: TalerProtocolTimestamp.fromSeconds(1), + exchange_pub: "response-key", + exchange_sig: "exchange-signature", + merge_timestamp: TalerProtocolTimestamp.fromSeconds(2), + } as any; + + await assert.doesNotReject( + requireValidExchangePurseStatus( + verificationContext({ cryptoValid: true, knownPub: "response-key" }), + "https://exchange.example/", + status, + ), + ); + await assert.rejects( + requireValidExchangePurseStatus( + verificationContext({ cryptoValid: false, knownPub: "response-key" }), + "https://exchange.example/", + status, + ), + /invalid purse status signature/, + ); + await assert.rejects( + requireValidExchangePurseStatus( + verificationContext({ cryptoValid: true, knownPub: "different-key" }), + "https://exchange.example/", + status, + ), + /invalid purse status signature/, + ); +}); + +test("direct refund confirmation requires signature and exchange key", async () => { + const args = { + exchangeBaseUrl: "https://exchange.example/", + contractTermsHash: "contract-hash", + coinPub: "coin-public-key", + merchantPub: "merchant-public-key", + rtransactionId: 1, + refundAmount: "TESTKUDOS:1" as AmountString, + response: { + exchange_pub: "response-key", + exchange_sig: "exchange-signature", + }, + }; + + await assert.doesNotReject( + requireValidDirectExchangeRefundConfirmation( + verificationContext({ cryptoValid: true, knownPub: "response-key" }), + args, + ), + ); + await assert.rejects( + requireValidDirectExchangeRefundConfirmation( + verificationContext({ cryptoValid: false, knownPub: "response-key" }), + args, + ), + /invalid refund confirmation signature/, + ); + await assert.rejects( + requireValidDirectExchangeRefundConfirmation( + verificationContext({ cryptoValid: true, knownPub: "different-key" }), + args, + ), + /invalid refund confirmation signature/, + ); +});