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:
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/,
+ );
+});