taler-typescript-core

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

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:
Mpackages/taler-wallet-core/src/pay-merchant.test.ts | 33+++++++++++++++++++++++++++++++++
Mpackages/taler-wallet-core/src/pay-merchant.ts | 29+++++++++++++++++++++++++++++
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,