From cb44202440313ea4405fbc74f4588144134a0821 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Wed, 12 Oct 2022 14:36:30 -0300 Subject: adding global fee info from exchange --- packages/taler-util/src/backupTypes.ts | 21 +++- packages/taler-util/src/payto.ts | 8 +- packages/taler-util/src/talerCrypto.ts | 1 + packages/taler-util/src/talerTypes.ts | 139 ++++++++++++++++----- .../src/crypto/cryptoImplementation.ts | 62 ++++++++- packages/taler-wallet-core/src/db.ts | 5 + .../src/operations/backup/export.ts | 1 + .../src/operations/backup/import.ts | 1 + .../taler-wallet-core/src/operations/exchanges.ts | 36 ++++++ 9 files changed, 233 insertions(+), 41 deletions(-) (limited to 'packages') diff --git a/packages/taler-util/src/backupTypes.ts b/packages/taler-util/src/backupTypes.ts index 620f476ad..8222bdeab 100644 --- a/packages/taler-util/src/backupTypes.ts +++ b/packages/taler-util/src/backupTypes.ts @@ -909,7 +909,7 @@ export interface BackupPurchase { /** * Signature on the contract terms. - * + * * FIXME: Better name needed. */ merchant_sig?: string; @@ -1086,6 +1086,23 @@ export interface BackupExchangeWireFee { sig: string; } +/** + * Global fee as stored in the wallet's database. + * + */ +export interface BackupExchangeGlobalFees { + start_date: TalerProtocolTimestamp; + end_date: TalerProtocolTimestamp; + kyc_fee: BackupAmountString; + history_fee: BackupAmountString; + account_fee: BackupAmountString; + purse_fee: BackupAmountString; + history_expiration: TalerProtocolDuration; + account_kyc_timeout: TalerProtocolDuration; + purse_account_limit: number; + purse_timeout: TalerProtocolDuration; + master_sig: string; +} /** * Structure of one exchange signing key in the /keys response. */ @@ -1206,6 +1223,8 @@ export interface BackupExchangeDetails { wire_fees: BackupExchangeWireFee[]; + global_fees: BackupExchangeGlobalFees[]; + /** * Bank accounts offered by the exchange; */ diff --git a/packages/taler-util/src/payto.ts b/packages/taler-util/src/payto.ts index c5a58022d..b474e533c 100644 --- a/packages/taler-util/src/payto.ts +++ b/packages/taler-util/src/payto.ts @@ -63,9 +63,9 @@ export function addPaytoQueryParams( ): string { const [acct, search] = s.slice(paytoPfx.length).split("?"); const searchParams = new URLSearchParams(search || ""); - const keys = Object.keys(params) + const keys = Object.keys(params); if (keys.length === 0) { - return paytoPfx + acct + return paytoPfx + acct; } for (const k of keys) { searchParams.set(k, params[k]); @@ -83,9 +83,7 @@ export function stringifyPaytoUri(p: PaytoUri): string { const url = `${paytoPfx}${p.targetType}/${p.targetPath}`; const paramList = !p.params ? [] : Object.entries(p.params); if (paramList.length > 0) { - const search = paramList - .map(([key, value]) => `${key}=${value}`) - .join("&"); + const search = paramList.map(([key, value]) => `${key}=${value}`).join("&"); return `${url}?${search}`; } return url; diff --git a/packages/taler-util/src/talerCrypto.ts b/packages/taler-util/src/talerCrypto.ts index 28fdab8e3..84842a69f 100644 --- a/packages/taler-util/src/talerCrypto.ts +++ b/packages/taler-util/src/talerCrypto.ts @@ -793,6 +793,7 @@ export enum TalerSignaturePurpose { MERCHANT_TRACK_TRANSACTION = 1103, WALLET_RESERVE_WITHDRAW = 1200, WALLET_COIN_DEPOSIT = 1201, + GLOBAL_FEES = 1022, MASTER_DENOMINATION_KEY_VALIDITY = 1025, MASTER_WIRE_FEES = 1028, MASTER_WIRE_DETAILS = 1030, diff --git a/packages/taler-util/src/talerTypes.ts b/packages/taler-util/src/talerTypes.ts index 471c7e927..1cb4e2bde 100644 --- a/packages/taler-util/src/talerTypes.ts +++ b/packages/taler-util/src/talerTypes.ts @@ -43,6 +43,7 @@ import { import { strcmp } from "./helpers.js"; import { AgeCommitmentProof, Edx25519PublicKeyEnc } from "./talerCrypto.js"; import { + codecForAbsoluteTime, codecForDuration, codecForTimestamp, TalerProtocolDuration, @@ -757,8 +758,64 @@ export class ExchangeKeysJson { version: string; reserve_closing_delay: TalerProtocolDuration; + + global_fees: GlobalFees[]; } +export interface GlobalFees { + // What date (inclusive) does these fees go into effect? + start_date: TalerProtocolTimestamp; + + // What date (exclusive) does this fees stop going into effect? + end_date: TalerProtocolTimestamp; + + // KYC fee, charged when a user wants to create an account. + // The first year of the account_annual_fee after the KYC is + // always included. + kyc_fee: AmountString; + + // Account history fee, charged when a user wants to + // obtain a reserve/account history. + history_fee: AmountString; + + // Annual fee charged for having an open account at the + // exchange. Charged to the account. If the account + // balance is insufficient to cover this fee, the account + // is automatically deleted/closed. (Note that the exchange + // will keep the account history around for longer for + // regulatory reasons.) + account_fee: AmountString; + + // Purse fee, charged only if a purse is abandoned + // and was not covered by the account limit. + purse_fee: AmountString; + + // How long will the exchange preserve the account history? + // After an account was deleted/closed, the exchange will + // retain the account history for legal reasons until this time. + history_expiration: TalerProtocolDuration; + + // How long does the exchange promise to keep funds + // an account for which the KYC has never happened + // after a purse was merged into an account? Basically, + // after this time funds in an account without KYC are + // forfeit. + account_kyc_timeout: TalerProtocolDuration; + + // Non-negative number of concurrent purses that any + // account holder is allowed to create without having + // to pay the purse_fee. + purse_account_limit: number; + + // How long does an exchange keep a purse around after a purse + // has expired (or been successfully merged)? A 'GET' request + // for a purse will succeed until the purse expiration time + // plus this value. + purse_timeout: TalerProtocolDuration; + + // Signature of TALER_GlobalFeesPS. + master_sig: string; +} /** * Wire fees as announced by the exchange. */ @@ -1188,13 +1245,6 @@ export namespace DenominationPubKey { } } -export const codecForDenominationPubKey = () => - buildCodecForUnion() - .discriminateOn("cipher") - .alternative(DenomKeyType.Rsa, codecForRsaDenominationPubKey()) - .alternative(DenomKeyType.ClauseSchnorr, codecForCsDenominationPubKey()) - .build("DenominationPubKey"); - export const codecForRsaDenominationPubKey = () => buildCodecForObject() .property("cipher", codecForConstString(DenomKeyType.Rsa)) @@ -1209,6 +1259,13 @@ export const codecForCsDenominationPubKey = () => .property("age_mask", codecForNumber()) .build("CsDenominationPubKey"); +export const codecForDenominationPubKey = () => + buildCodecForUnion() + .discriminateOn("cipher") + .alternative(DenomKeyType.Rsa, codecForRsaDenominationPubKey()) + .alternative(DenomKeyType.ClauseSchnorr, codecForCsDenominationPubKey()) + .build("DenominationPubKey"); + export const codecForBankWithdrawalOperationPostResponse = (): Codec => buildCodecForObject() @@ -1385,6 +1442,21 @@ export const codecForExchangeSigningKey = (): Codec => .property("stamp_expire", codecForTimestamp) .build("ExchangeSignKeyJson"); +export const codecForGlobalFees = (): Codec => + buildCodecForObject() + .property("start_date", codecForTimestamp) + .property("end_date", codecForTimestamp) + .property("kyc_fee", codecForAmountString()) + .property("history_fee", codecForAmountString()) + .property("account_fee", codecForAmountString()) + .property("purse_fee", codecForAmountString()) + .property("history_expiration", codecForDuration) + .property("account_kyc_timeout", codecForDuration) + .property("purse_account_limit", codecForNumber()) + .property("purse_timeout", codecForDuration) + .property("master_sig", codecForString()) + .build("GlobalFees"); + export const codecForExchangeKeysJson = (): Codec => buildCodecForObject() .property("denoms", codecForList(codecForDenomination())) @@ -1395,6 +1467,7 @@ export const codecForExchangeKeysJson = (): Codec => .property("signkeys", codecForList(codecForExchangeSigningKey())) .property("version", codecForString()) .property("reserve_closing_delay", codecForDuration) + .property("global_fees", codecForList(codecForGlobalFees())) .build("ExchangeKeysJson"); export const codecForWireFeesJson = (): Codec => @@ -1583,6 +1656,32 @@ export interface AbortResponse { refunds: MerchantAbortPayRefundStatus[]; } +export const codecForMerchantAbortPayRefundSuccessStatus = + (): Codec => + buildCodecForObject() + .property("exchange_pub", codecForString()) + .property("exchange_sig", codecForString()) + .property("exchange_status", codecForConstNumber(200)) + .property("type", codecForConstString("success")) + .build("MerchantAbortPayRefundSuccessStatus"); + +export const codecForMerchantAbortPayRefundFailureStatus = + (): Codec => + buildCodecForObject() + .property("exchange_code", codecForNumber()) + .property("exchange_reply", codecForAny()) + .property("exchange_status", codecForNumber()) + .property("type", codecForConstString("failure")) + .build("MerchantAbortPayRefundFailureStatus"); + +export const codecForMerchantAbortPayRefundStatus = + (): Codec => + buildCodecForUnion() + .discriminateOn("type") + .alternative("success", codecForMerchantAbortPayRefundSuccessStatus()) + .alternative("failure", codecForMerchantAbortPayRefundFailureStatus()) + .build("MerchantAbortPayRefundStatus"); + export const codecForAbortResponse = (): Codec => buildCodecForObject() .property("refunds", codecForList(codecForMerchantAbortPayRefundStatus())) @@ -1629,32 +1728,6 @@ export interface MerchantAbortPayRefundSuccessStatus { exchange_pub: string; } -export const codecForMerchantAbortPayRefundSuccessStatus = - (): Codec => - buildCodecForObject() - .property("exchange_pub", codecForString()) - .property("exchange_sig", codecForString()) - .property("exchange_status", codecForConstNumber(200)) - .property("type", codecForConstString("success")) - .build("MerchantAbortPayRefundSuccessStatus"); - -export const codecForMerchantAbortPayRefundFailureStatus = - (): Codec => - buildCodecForObject() - .property("exchange_code", codecForNumber()) - .property("exchange_reply", codecForAny()) - .property("exchange_status", codecForNumber()) - .property("type", codecForConstString("failure")) - .build("MerchantAbortPayRefundFailureStatus"); - -export const codecForMerchantAbortPayRefundStatus = - (): Codec => - buildCodecForUnion() - .discriminateOn("type") - .alternative("success", codecForMerchantAbortPayRefundSuccessStatus()) - .alternative("failure", codecForMerchantAbortPayRefundFailureStatus()) - .build("MerchantAbortPayRefundStatus"); - export interface TalerConfigResponse { name: string; version: string; diff --git a/packages/taler-wallet-core/src/crypto/cryptoImplementation.ts b/packages/taler-wallet-core/src/crypto/cryptoImplementation.ts index bfc48d961..98bb6c9cb 100644 --- a/packages/taler-wallet-core/src/crypto/cryptoImplementation.ts +++ b/packages/taler-wallet-core/src/crypto/cryptoImplementation.ts @@ -51,6 +51,7 @@ import { encryptContractForMerge, ExchangeProtocolVersion, getRandomBytes, + GlobalFees, hash, HashCodeString, hashCoinEv, @@ -74,6 +75,7 @@ import { rsaVerify, setupTipPlanchet, stringToBytes, + TalerProtocolDuration, TalerProtocolTimestamp, TalerSignaturePurpose, UnblindedSignature, @@ -142,6 +144,10 @@ export interface TalerCryptoInterface { isValidWireFee(req: WireFeeValidationRequest): Promise; + isValidGlobalFees( + req: GlobalFeesValidationRequest, + ): Promise; + isValidDenom(req: DenominationValidationRequest): Promise; isValidWireAccount( @@ -152,7 +158,7 @@ export interface TalerCryptoInterface { req: ContractTermsValidationRequest, ): Promise; - createEddsaKeypair(req: {}): Promise; + createEddsaKeypair(req: unknown): Promise; eddsaGetPublic(req: EddsaGetPublicRequest): Promise; @@ -283,12 +289,17 @@ export const nullCrypto: TalerCryptoInterface = { ): Promise { throw new Error("Function not implemented."); }, + isValidGlobalFees: function ( + req: GlobalFeesValidationRequest, + ): Promise { + throw new Error("Function not implemented."); + }, isValidContractTermsSignature: function ( req: ContractTermsValidationRequest, ): Promise { throw new Error("Function not implemented."); }, - createEddsaKeypair: function (req: {}): Promise { + createEddsaKeypair: function (req: unknown): Promise { throw new Error("Function not implemented."); }, eddsaGetPublic: function (req: EddsaGetPublicRequest): Promise { @@ -484,6 +495,11 @@ export interface WireFeeValidationRequest { masterPub: string; } +export interface GlobalFeesValidationRequest { + gf: GlobalFees; + masterPub: string; +} + export interface DenominationValidationRequest { denom: DenominationRecord; masterPub: string; @@ -887,6 +903,30 @@ export const nativeCryptoR: TalerCryptoInterfaceR = { return { valid: eddsaVerify(p, sig, pub) }; }, + /** + * Check if a global fee is correctly signed. + */ + async isValidGlobalFees( + tci: TalerCryptoInterfaceR, + req: GlobalFeesValidationRequest, + ): Promise { + const { gf, masterPub } = req; + const p = buildSigPS(TalerSignaturePurpose.GLOBAL_FEES) + .put(timestampRoundedToBuffer(gf.start_date)) + .put(timestampRoundedToBuffer(gf.end_date)) + .put(durationRoundedToBuffer(gf.purse_timeout)) + .put(durationRoundedToBuffer(gf.account_kyc_timeout)) + .put(durationRoundedToBuffer(gf.history_expiration)) + .put(amountToBuffer(Amounts.parseOrThrow(gf.history_fee))) + .put(amountToBuffer(Amounts.parseOrThrow(gf.kyc_fee))) + .put(amountToBuffer(Amounts.parseOrThrow(gf.account_fee))) + .put(amountToBuffer(Amounts.parseOrThrow(gf.purse_fee))) + .put(bufferForUint32(gf.purse_account_limit)) + .build(); + const sig = decodeCrock(gf.master_sig); + const pub = decodeCrock(masterPub); + return { valid: eddsaVerify(p, sig, pub) }; + }, /** * Check if the signature of a denomination is valid. */ @@ -1630,6 +1670,24 @@ function timestampRoundedToBuffer(ts: TalerProtocolTimestamp): Uint8Array { return new Uint8Array(b); } +function durationRoundedToBuffer(ts: TalerProtocolDuration): Uint8Array { + const b = new ArrayBuffer(8); + const v = new DataView(b); + // The buffer we sign over represents the timestamp in microseconds. + if (typeof v.setBigUint64 !== "undefined") { + const s = BigInt(ts.d_us); + v.setBigUint64(0, s); + } else { + const s = ts.d_us === "forever" ? bigint.zero : bigint(ts.d_us); + const arr = s.toArray(2 ** 8).value; + let offset = 8 - arr.length; + for (let i = 0; i < arr.length; i++) { + v.setUint8(offset++, arr[i]); + } + } + return new Uint8Array(b); +} + export interface EddsaSignRequest { msg: string; priv: string; diff --git a/packages/taler-wallet-core/src/db.ts b/packages/taler-wallet-core/src/db.ts index ec11f4d47..e266275c1 100644 --- a/packages/taler-wallet-core/src/db.ts +++ b/packages/taler-wallet-core/src/db.ts @@ -45,6 +45,7 @@ import { Location, WireInfo, DenominationInfo, + GlobalFees, } from "@gnu-taler/taler-util"; import { RetryInfo, RetryTags } from "./util/retries.js"; import { Event, IDBDatabase } from "@gnu-taler/idb-bridge"; @@ -424,6 +425,10 @@ export interface ExchangeDetailsRecord { reserveClosingDelay: TalerProtocolDuration; + /** + * Fees for exchange services + */ + globalFees: GlobalFees[]; /** * Signing keys we got from the exchange, can also contain * older signing keys that are not returned by /keys anymore. diff --git a/packages/taler-wallet-core/src/operations/backup/export.ts b/packages/taler-wallet-core/src/operations/backup/export.ts index 2e2a1c4b4..f611a2380 100644 --- a/packages/taler-wallet-core/src/operations/backup/export.ts +++ b/packages/taler-wallet-core/src/operations/backup/export.ts @@ -345,6 +345,7 @@ export async function exportBackup( stamp_expire: x.stamp_expire, stamp_start: x.stamp_start, })), + global_fees: ex.globalFees, tos_accepted_etag: ex.termsOfServiceAcceptedEtag, tos_accepted_timestamp: ex.termsOfServiceAcceptedTimestamp, denominations: diff --git a/packages/taler-wallet-core/src/operations/backup/import.ts b/packages/taler-wallet-core/src/operations/backup/import.ts index 3ee3680fe..ee8cb6f6c 100644 --- a/packages/taler-wallet-core/src/operations/backup/import.ts +++ b/packages/taler-wallet-core/src/operations/backup/import.ts @@ -405,6 +405,7 @@ export async function importBackup( masterPublicKey: backupExchangeDetails.master_public_key, protocolVersion: backupExchangeDetails.protocol_version, reserveClosingDelay: backupExchangeDetails.reserve_closing_delay, + globalFees: backupExchangeDetails.global_fees, signingKeys: backupExchangeDetails.signing_keys.map((x) => ({ key: x.key, master_sig: x.master_sig, diff --git a/packages/taler-wallet-core/src/operations/exchanges.ts b/packages/taler-wallet-core/src/operations/exchanges.ts index 9a6c72577..a26c14fcc 100644 --- a/packages/taler-wallet-core/src/operations/exchanges.ts +++ b/packages/taler-wallet-core/src/operations/exchanges.ts @@ -32,6 +32,7 @@ import { ExchangeDenomination, ExchangeSignKeyJson, ExchangeWireJson, + GlobalFees, hashDenomPub, j2s, LibtoolVersion, @@ -269,6 +270,32 @@ async function validateWireInfo( }; } +async function validateGlobalFees( + ws: InternalWalletState, + fees: GlobalFees[], + masterPub: string, +): Promise { + for (const gf of fees) { + logger.trace("validating exchange global fees"); + let isValid = false; + if (ws.insecureTrustExchange) { + isValid = true; + } else { + const { valid: v } = await ws.cryptoApi.isValidGlobalFees({ + masterPub, + gf, + }); + isValid = v; + } + + if (!isValid) { + throw Error("exchange global fees signature invalid: " + gf.master_sig); + } + } + + return fees; +} + export interface ExchangeInfo { wire: ExchangeWireJson; keys: ExchangeKeysDownloadResult; @@ -359,6 +386,7 @@ interface ExchangeKeysDownloadResult { expiry: TalerProtocolTimestamp; recoup: Recoup[]; listIssueDate: TalerProtocolTimestamp; + globalFees: GlobalFees[]; } /** @@ -432,6 +460,7 @@ async function downloadExchangeKeysInfo( ), recoup: exchangeKeysJsonUnchecked.recoup ?? [], listIssueDate: exchangeKeysJsonUnchecked.list_issue_date, + globalFees: exchangeKeysJsonUnchecked.global_fees, }; } @@ -552,6 +581,12 @@ export async function updateExchangeFromUrlHandler( keysInfo.masterPublicKey, ); + const globalFees = await validateGlobalFees( + ws, + keysInfo.globalFees, + keysInfo.masterPublicKey, + ); + logger.info("finished validating exchange /wire info"); const tosDownload = await downloadTosFromAcceptedFormat( @@ -594,6 +629,7 @@ export async function updateExchangeFromUrlHandler( protocolVersion: keysInfo.protocolVersion, signingKeys: keysInfo.signingKeys, reserveClosingDelay: keysInfo.reserveClosingDelay, + globalFees, exchangeBaseUrl: r.baseUrl, wireInfo, termsOfServiceText: tosDownload.tosText, -- cgit v1.2.3