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:
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.
*/