taler-typescript-core

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

commit 977baec467762efdf19eac21a6a5e00336b32e60
parent 82ae3ed98955f1ec5a0092ff95c3fa1dfcb75406
Author: Florian Dold <dold@taler.net>
Date:   Thu, 20 Aug 2026 19:06:47 +0200

wallet-core: verify refund and purse signatures

Diffstat:
Mpackages/taler-wallet-core/src/crypto/cryptoImplementation.test.ts | 75+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mpackages/taler-wallet-core/src/crypto/cryptoImplementation.ts | 82+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 157 insertions(+), 0 deletions(-)

diff --git a/packages/taler-wallet-core/src/crypto/cryptoImplementation.test.ts b/packages/taler-wallet-core/src/crypto/cryptoImplementation.test.ts @@ -18,6 +18,7 @@ import { Amounts, buildSigPS, bufferForUint32, + bufferForUint64, bufferFromAmount, createEddsaKeyPair, decodeCrock, @@ -153,6 +154,80 @@ test("melt confirmation binds the refresh commitment and noreveal index", async ); }); +test("refund confirmation binds the contract, parties, transaction and amount", async () => { + const contractTermsHash = encodeCrock(new Uint8Array(64).fill(1)); + const coinPub = encodeCrock(new Uint8Array(32).fill(2)); + const merchantPub = encodeCrock(new Uint8Array(32).fill(3)); + const rtransactionId = 42; + const refundAmount = "TESTKUDOS:1.5" as AmountString; + const sigBlob = buildSigPS(TalerSignaturePurpose.EXCHANGE_CONFIRM_REFUND) + .put(decodeCrock(contractTermsHash)) + .put(decodeCrock(coinPub)) + .put(decodeCrock(merchantPub)) + .put(bufferForUint64(rtransactionId)) + .put(bufferFromAmount(Amounts.parseOrThrow(refundAmount))) + .build(); + const exchangePub = encodeCrock(signKey.eddsaPub) as EddsaPublicKeyString; + const exchangeSig = encodeCrock( + eddsaSign(sigBlob, signKey.eddsaPriv), + ) as EddsaSignatureString; + const request = { + contractTermsHash, + coinPub, + merchantPub, + rtransactionId, + refundAmount, + exchangePub, + exchangeSig, + }; + + assert.deepStrictEqual( + await nativeCryptoR.isValidRefundConfirmation(nativeCryptoR, request), + { valid: true }, + ); + assert.deepStrictEqual( + await nativeCryptoR.isValidRefundConfirmation(nativeCryptoR, { + ...request, + refundAmount: "TESTKUDOS:1.6", + }), + { valid: false }, + ); +}); + +test("purse status signature binds timestamps and balance", async () => { + const balance = "TESTKUDOS:4" as AmountString; + const depositTimestamp = t(1234); + const mergeTimestamp = t(1250); + const sigBlob = buildSigPS(TalerSignaturePurpose.EXCHANGE_PURSE_STATUS) + .put(timestampRoundedToBuffer(mergeTimestamp)) + .put(timestampRoundedToBuffer(depositTimestamp)) + .put(bufferFromAmount(Amounts.parseOrThrow(balance))) + .build(); + const exchangePub = encodeCrock(signKey.eddsaPub) as EddsaPublicKeyString; + const exchangeSig = encodeCrock( + eddsaSign(sigBlob, signKey.eddsaPriv), + ) as EddsaSignatureString; + const request = { + balance, + depositTimestamp, + mergeTimestamp, + exchangePub, + exchangeSig, + }; + + assert.deepStrictEqual( + await nativeCryptoR.isValidPurseStatus(nativeCryptoR, request), + { valid: true }, + ); + assert.deepStrictEqual( + await nativeCryptoR.isValidPurseStatus(nativeCryptoR, { + ...request, + balance: "TESTKUDOS:5", + }), + { valid: false }, + ); +}); + test("a correctly signed exchange signing key is accepted", async () => { const res = await nativeCryptoR.isValidSignKey(nativeCryptoR, { masterPub, diff --git a/packages/taler-wallet-core/src/crypto/cryptoImplementation.ts b/packages/taler-wallet-core/src/crypto/cryptoImplementation.ts @@ -262,6 +262,14 @@ export interface TalerCryptoInterface { req: MeltConfirmationValidationRequest, ): Promise<ValidationResult>; + isValidRefundConfirmation( + req: RefundConfirmationValidationRequest, + ): Promise<ValidationResult>; + + isValidPurseStatus( + req: PurseStatusValidationRequest, + ): Promise<ValidationResult>; + isValidWireFee(req: WireFeeValidationRequest): Promise<ValidationResult>; isValidGlobalFees( @@ -460,6 +468,16 @@ export const nullCrypto: TalerCryptoInterface = { ): Promise<ValidationResult> { throw new Error("Function not implemented."); }, + isValidRefundConfirmation: function ( + req: RefundConfirmationValidationRequest, + ): Promise<ValidationResult> { + throw new Error("Function not implemented."); + }, + isValidPurseStatus: function ( + req: PurseStatusValidationRequest, + ): Promise<ValidationResult> { + throw new Error("Function not implemented."); + }, isValidWireFee: function ( req: WireFeeValidationRequest, ): Promise<ValidationResult> { @@ -877,6 +895,24 @@ export interface MeltConfirmationValidationRequest { exchangeSig: EddsaSignatureString; } +export interface RefundConfirmationValidationRequest { + contractTermsHash: HashCodeString; + coinPub: EddsaPublicKeyString; + merchantPub: EddsaPublicKeyString; + rtransactionId: number; + refundAmount: AmountString; + exchangePub: EddsaPublicKeyString; + exchangeSig: EddsaSignatureString; +} + +export interface PurseStatusValidationRequest { + balance: AmountString; + depositTimestamp?: TalerProtocolTimestamp; + mergeTimestamp?: TalerProtocolTimestamp; + exchangePub: EddsaPublicKeyString; + exchangeSig: EddsaSignatureString; +} + export interface ContractTermsValidationRequest { contractTermsHash: string; sig: string; @@ -1421,6 +1457,52 @@ export const nativeCryptoR: TalerCryptoInterfaceR = { }; }, + async isValidRefundConfirmation( + tci: TalerCryptoInterfaceR, + req: RefundConfirmationValidationRequest, + ): Promise<ValidationResult> { + const p = buildSigPS(TalerSignaturePurpose.EXCHANGE_CONFIRM_REFUND) + .put(decodeCrock(req.contractTermsHash)) + .put(decodeCrock(req.coinPub)) + .put(decodeCrock(req.merchantPub)) + .put(bufferForUint64(req.rtransactionId)) + .put(bufferFromAmount(Amounts.parseOrThrow(req.refundAmount))) + .build(); + return { + valid: eddsaVerify( + p, + decodeCrock(req.exchangeSig), + decodeCrock(req.exchangePub), + ), + }; + }, + + async isValidPurseStatus( + tci: TalerCryptoInterfaceR, + req: PurseStatusValidationRequest, + ): Promise<ValidationResult> { + const p = buildSigPS(TalerSignaturePurpose.EXCHANGE_PURSE_STATUS) + .put( + timestampRoundedToBuffer( + req.mergeTimestamp ?? TalerProtocolTimestamp.never(), + ), + ) + .put( + timestampRoundedToBuffer( + req.depositTimestamp ?? TalerProtocolTimestamp.never(), + ), + ) + .put(bufferFromAmount(Amounts.parseOrThrow(req.balance))) + .build(); + return { + valid: eddsaVerify( + p, + decodeCrock(req.exchangeSig), + decodeCrock(req.exchangePub), + ), + }; + }, + /** * Check if a wire fee is correctly signed. */