commit dec00d5919deb70850649dbe3e323aae8951e87e
parent b3b87417f1e1bf42fa2aad4b93167764a470f265
Author: Florian Dold <dold@taler.net>
Date: Sun, 13 Sep 2026 16:16:09 +0200
wallet-core: reject invalid merchant contract signatures
Check the validity field returned by signature verification instead of
treating the result object itself as a boolean. Invalid signatures must
fail the claim before contract terms are stored.
Diffstat:
2 files changed, 97 insertions(+), 5 deletions(-)
diff --git a/packages/taler-wallet-core/src/pay-merchant.test.ts b/packages/taler-wallet-core/src/pay-merchant.test.ts
@@ -16,6 +16,12 @@
import {
Amounts,
AmountString,
+ buildSigPS,
+ ContractTermsUtil,
+ decodeCrock,
+ eddsaGetPublic,
+ eddsaSign,
+ encodeCrock,
HttpStatusCode,
MerchantContractVersion,
RefreshReason,
@@ -26,6 +32,7 @@ import {
TalerPreciseTimestamp,
TalerError,
TalerErrorCode,
+ TalerSignaturePurpose,
TimerAPI,
TimerGroup,
TimerHandle,
@@ -59,6 +66,7 @@ import {
failProposalClaimPermanently,
FAILED_CLAIM_RETENTION_MS,
preparePayForUriV2,
+ processPurchase,
generateTokenSigs,
getCoinsToSpendForMerchantRepair,
getAlreadyPaidRefundRequests,
@@ -83,6 +91,8 @@ import {
} from "./pay-merchant.js";
import { makeIdbRunner } from "./db/testing/runners.js";
import type { WalletExecutionContext } from "./wallet.js";
+import { nativeCrypto } from "./crypto/cryptoImplementation.js";
+import { HeadersImpl } from "@gnu-taler/taler-util/http";
interface ScheduledTimer {
due: number;
@@ -159,6 +169,86 @@ function claimPurchase(
};
}
+test("proposal download accepts only valid merchant contract signatures", async () => {
+ const runner = await makeIdbRunner();
+ try {
+ for (const valid of [false, true]) {
+ const proposal = claimPurchase(`claim-signature-${valid}`);
+ const deadline = { t_s: Math.floor(Date.now() / 1000) + 86400 };
+ const priv = new Uint8Array(32).fill(9);
+ const contract = {
+ amount: "TEST:1",
+ max_fee: "TEST:0",
+ order_id: proposal.orderId,
+ nonce: proposal.noncePub,
+ merchant_base_url: proposal.merchantBaseUrl,
+ h_wire: encodeCrock(new Uint8Array(64)),
+ wire_method: "iban",
+ summary: "signature test",
+ pay_deadline: deadline,
+ refund_deadline: deadline,
+ wire_transfer_deadline: deadline,
+ timestamp: { t_s: Math.floor(Date.now() / 1000) },
+ merchant: { name: "review" },
+ merchant_pub: encodeCrock(eddsaGetPublic(priv)),
+ exchanges: [],
+ };
+ const h = ContractTermsUtil.hashContractTerms(contract);
+ const signature = eddsaSign(
+ buildSigPS(TalerSignaturePurpose.MERCHANT_CONTRACT)
+ .put(decodeCrock(h))
+ .build(),
+ priv,
+ );
+ const body = {
+ contract_terms: contract,
+ sig: encodeCrock(valid ? signature : new Uint8Array(64)),
+ };
+ const wex = {
+ ws: {
+ networkAvailable: true,
+ leftoverClaims: new Set(),
+ timerGroup: new ManualTimer(),
+ },
+ runWalletDbTx: <T>(f: (tx: WalletDbTransaction) => Promise<T>) =>
+ runner.runReadWriteTx(f),
+ cryptoApi: nativeCrypto,
+ http: {
+ fetch: async (url: string) => ({
+ status: 200,
+ requestUrl: url,
+ requestMethod: "POST",
+ headers: new HeadersImpl(),
+ json: async () => body,
+ text: async () => JSON.stringify(body),
+ }),
+ },
+ } as unknown as WalletExecutionContext;
+ await runner.runReadWriteTx((tx) => tx.upsertPurchase(proposal));
+ await processPurchase(wex, proposal.proposalId);
+ const stored = await runner.runReadWriteTx((tx) =>
+ tx.getPurchase(proposal.proposalId),
+ );
+ assert.strictEqual(
+ stored?.purchaseStatus,
+ valid ? PurchaseStatus.DialogProposed : PurchaseStatus.FailedClaim,
+ );
+ const terms = await runner.runReadWriteTx((tx) => tx.getContractTerms(h));
+ if (valid) assert.ok(terms);
+ else {
+ assert.strictEqual(terms, undefined);
+ assert.strictEqual(stored?.download, undefined);
+ assert.strictEqual(
+ stored?.failReason?.code,
+ TalerErrorCode.WALLET_CONTRACT_TERMS_SIGNATURE_INVALID,
+ );
+ }
+ }
+ } finally {
+ await runner.close();
+ }
+});
+
test("payment-share flags use the shared transaction path", () => {
assert.strictEqual(
isSharedPurchase({ shared: true, createdFromShared: false }),
diff --git a/packages/taler-wallet-core/src/pay-merchant.ts b/packages/taler-wallet-core/src/pay-merchant.ts
@@ -1663,11 +1663,13 @@ async function processDownloadProposal(
return TaskRunResult.finished();
}
- const sigValid = await wex.cryptoApi.isValidContractTermsSignature({
- contractTermsHash,
- merchantPub: parsedContractTerms.merchant_pub,
- sig: proposalResp.sig,
- });
+ const { valid: sigValid } = await wex.cryptoApi.isValidContractTermsSignature(
+ {
+ contractTermsHash,
+ merchantPub: parsedContractTerms.merchant_pub,
+ sig: proposalResp.sig,
+ },
+ );
if (!sigValid) {
const err = makeErrorDetail(