commit 2ad56046aad4b172e6e75bdfb7d88c8bb27cdfc5
parent 94b1297b966b2b45b8f7a5f15d42f8ef046f8813
Author: Florian Dold <dold@taler.net>
Date: Thu, 20 Aug 2026 19:06:46 +0200
wallet-core: bind claimed contracts to requests
Diffstat:
2 files changed, 62 insertions(+), 0 deletions(-)
diff --git a/packages/taler-wallet-core/src/pay-merchant.test.ts b/packages/taler-wallet-core/src/pay-merchant.test.ts
@@ -34,6 +34,7 @@ import {
releasePaymentTokensInTx,
setRefundGroupEffectiveAmount,
splitPaymentOutputTokenSignatures,
+ validateClaimResponseBindings,
} from "./pay-merchant.js";
function makeSelectedCoin(
@@ -172,3 +173,35 @@ test("refund totals and effective amounts reflect completed items", () => {
effective: "TESTKUDOS:3",
});
});
+
+test("claimed contract terms are bound to the requested order and nonce", () => {
+ const contractTerms = {
+ order_id: "requested-order",
+ nonce: "requested-nonce",
+ } as unknown as Parameters<typeof validateClaimResponseBindings>[2];
+
+ assert.strictEqual(
+ validateClaimResponseBindings(
+ "requested-order",
+ "requested-nonce",
+ contractTerms,
+ ),
+ undefined,
+ );
+ assert.match(
+ validateClaimResponseBindings(
+ "different-order",
+ "requested-nonce",
+ contractTerms,
+ ) ?? "",
+ /order ID/,
+ );
+ assert.match(
+ validateClaimResponseBindings(
+ "requested-order",
+ "different-nonce",
+ contractTerms,
+ ) ?? "",
+ /wallet nonce/,
+ );
+});
diff --git a/packages/taler-wallet-core/src/pay-merchant.ts b/packages/taler-wallet-core/src/pay-merchant.ts
@@ -1240,6 +1240,21 @@ async function processDownloadProposal(
return TaskRunResult.finished();
}
+ const bindingError = validateClaimResponseBindings(
+ proposal.orderId,
+ proposal.noncePub,
+ parsedContractTerms,
+ );
+ if (bindingError) {
+ const err = makeErrorDetail(
+ TalerErrorCode.WALLET_CONTRACT_TERMS_MALFORMED,
+ {},
+ bindingError,
+ );
+ await discardClaim(wex, proposalId, err);
+ return TaskRunResult.finished();
+ }
+
const sigValid = await wex.cryptoApi.isValidContractTermsSignature({
contractTermsHash,
merchantPub: parsedContractTerms.merchant_pub,
@@ -1382,6 +1397,20 @@ async function processDownloadProposal(
return TaskRunResult.progress();
}
+export function validateClaimResponseBindings(
+ expectedOrderId: string,
+ expectedNonce: string,
+ contractTerms: MerchantContractTerms,
+): string | undefined {
+ if (contractTerms.order_id !== expectedOrderId) {
+ return "merchant contract terms contain a different order ID";
+ }
+ if (contractTerms.nonce !== expectedNonce) {
+ return "merchant contract terms contain a different wallet nonce";
+ }
+ return undefined;
+}
+
async function startPayReplay(
wex: WalletExecutionContext,
tx: WalletDbTransaction,