adding global fee info from exchange

This commit is contained in:
Sebastian 2022-10-12 14:36:30 -03:00
parent ef95914cfc
commit cb44202440
No known key found for this signature in database
GPG Key ID: BE4FF68352439FC1
9 changed files with 233 additions and 41 deletions

View File

@ -1086,6 +1086,23 @@ export interface BackupExchangeWireFee {
sig: string; 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. * Structure of one exchange signing key in the /keys response.
*/ */
@ -1206,6 +1223,8 @@ export interface BackupExchangeDetails {
wire_fees: BackupExchangeWireFee[]; wire_fees: BackupExchangeWireFee[];
global_fees: BackupExchangeGlobalFees[];
/** /**
* Bank accounts offered by the exchange; * Bank accounts offered by the exchange;
*/ */

View File

@ -63,9 +63,9 @@ export function addPaytoQueryParams(
): string { ): string {
const [acct, search] = s.slice(paytoPfx.length).split("?"); const [acct, search] = s.slice(paytoPfx.length).split("?");
const searchParams = new URLSearchParams(search || ""); const searchParams = new URLSearchParams(search || "");
const keys = Object.keys(params) const keys = Object.keys(params);
if (keys.length === 0) { if (keys.length === 0) {
return paytoPfx + acct return paytoPfx + acct;
} }
for (const k of keys) { for (const k of keys) {
searchParams.set(k, params[k]); searchParams.set(k, params[k]);
@ -83,9 +83,7 @@ export function stringifyPaytoUri(p: PaytoUri): string {
const url = `${paytoPfx}${p.targetType}/${p.targetPath}`; const url = `${paytoPfx}${p.targetType}/${p.targetPath}`;
const paramList = !p.params ? [] : Object.entries(p.params); const paramList = !p.params ? [] : Object.entries(p.params);
if (paramList.length > 0) { if (paramList.length > 0) {
const search = paramList const search = paramList.map(([key, value]) => `${key}=${value}`).join("&");
.map(([key, value]) => `${key}=${value}`)
.join("&");
return `${url}?${search}`; return `${url}?${search}`;
} }
return url; return url;

View File

@ -793,6 +793,7 @@ export enum TalerSignaturePurpose {
MERCHANT_TRACK_TRANSACTION = 1103, MERCHANT_TRACK_TRANSACTION = 1103,
WALLET_RESERVE_WITHDRAW = 1200, WALLET_RESERVE_WITHDRAW = 1200,
WALLET_COIN_DEPOSIT = 1201, WALLET_COIN_DEPOSIT = 1201,
GLOBAL_FEES = 1022,
MASTER_DENOMINATION_KEY_VALIDITY = 1025, MASTER_DENOMINATION_KEY_VALIDITY = 1025,
MASTER_WIRE_FEES = 1028, MASTER_WIRE_FEES = 1028,
MASTER_WIRE_DETAILS = 1030, MASTER_WIRE_DETAILS = 1030,

View File

@ -43,6 +43,7 @@ import {
import { strcmp } from "./helpers.js"; import { strcmp } from "./helpers.js";
import { AgeCommitmentProof, Edx25519PublicKeyEnc } from "./talerCrypto.js"; import { AgeCommitmentProof, Edx25519PublicKeyEnc } from "./talerCrypto.js";
import { import {
codecForAbsoluteTime,
codecForDuration, codecForDuration,
codecForTimestamp, codecForTimestamp,
TalerProtocolDuration, TalerProtocolDuration,
@ -757,8 +758,64 @@ export class ExchangeKeysJson {
version: string; version: string;
reserve_closing_delay: TalerProtocolDuration; 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. * Wire fees as announced by the exchange.
*/ */
@ -1188,13 +1245,6 @@ export namespace DenominationPubKey {
} }
} }
export const codecForDenominationPubKey = () =>
buildCodecForUnion<DenominationPubKey>()
.discriminateOn("cipher")
.alternative(DenomKeyType.Rsa, codecForRsaDenominationPubKey())
.alternative(DenomKeyType.ClauseSchnorr, codecForCsDenominationPubKey())
.build("DenominationPubKey");
export const codecForRsaDenominationPubKey = () => export const codecForRsaDenominationPubKey = () =>
buildCodecForObject<RsaDenominationPubKey>() buildCodecForObject<RsaDenominationPubKey>()
.property("cipher", codecForConstString(DenomKeyType.Rsa)) .property("cipher", codecForConstString(DenomKeyType.Rsa))
@ -1209,6 +1259,13 @@ export const codecForCsDenominationPubKey = () =>
.property("age_mask", codecForNumber()) .property("age_mask", codecForNumber())
.build("CsDenominationPubKey"); .build("CsDenominationPubKey");
export const codecForDenominationPubKey = () =>
buildCodecForUnion<DenominationPubKey>()
.discriminateOn("cipher")
.alternative(DenomKeyType.Rsa, codecForRsaDenominationPubKey())
.alternative(DenomKeyType.ClauseSchnorr, codecForCsDenominationPubKey())
.build("DenominationPubKey");
export const codecForBankWithdrawalOperationPostResponse = export const codecForBankWithdrawalOperationPostResponse =
(): Codec<BankWithdrawalOperationPostResponse> => (): Codec<BankWithdrawalOperationPostResponse> =>
buildCodecForObject<BankWithdrawalOperationPostResponse>() buildCodecForObject<BankWithdrawalOperationPostResponse>()
@ -1385,6 +1442,21 @@ export const codecForExchangeSigningKey = (): Codec<ExchangeSignKeyJson> =>
.property("stamp_expire", codecForTimestamp) .property("stamp_expire", codecForTimestamp)
.build("ExchangeSignKeyJson"); .build("ExchangeSignKeyJson");
export const codecForGlobalFees = (): Codec<GlobalFees> =>
buildCodecForObject<GlobalFees>()
.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<ExchangeKeysJson> => export const codecForExchangeKeysJson = (): Codec<ExchangeKeysJson> =>
buildCodecForObject<ExchangeKeysJson>() buildCodecForObject<ExchangeKeysJson>()
.property("denoms", codecForList(codecForDenomination())) .property("denoms", codecForList(codecForDenomination()))
@ -1395,6 +1467,7 @@ export const codecForExchangeKeysJson = (): Codec<ExchangeKeysJson> =>
.property("signkeys", codecForList(codecForExchangeSigningKey())) .property("signkeys", codecForList(codecForExchangeSigningKey()))
.property("version", codecForString()) .property("version", codecForString())
.property("reserve_closing_delay", codecForDuration) .property("reserve_closing_delay", codecForDuration)
.property("global_fees", codecForList(codecForGlobalFees()))
.build("ExchangeKeysJson"); .build("ExchangeKeysJson");
export const codecForWireFeesJson = (): Codec<WireFeesJson> => export const codecForWireFeesJson = (): Codec<WireFeesJson> =>
@ -1583,6 +1656,32 @@ export interface AbortResponse {
refunds: MerchantAbortPayRefundStatus[]; refunds: MerchantAbortPayRefundStatus[];
} }
export const codecForMerchantAbortPayRefundSuccessStatus =
(): Codec<MerchantAbortPayRefundSuccessStatus> =>
buildCodecForObject<MerchantAbortPayRefundSuccessStatus>()
.property("exchange_pub", codecForString())
.property("exchange_sig", codecForString())
.property("exchange_status", codecForConstNumber(200))
.property("type", codecForConstString("success"))
.build("MerchantAbortPayRefundSuccessStatus");
export const codecForMerchantAbortPayRefundFailureStatus =
(): Codec<MerchantAbortPayRefundFailureStatus> =>
buildCodecForObject<MerchantAbortPayRefundFailureStatus>()
.property("exchange_code", codecForNumber())
.property("exchange_reply", codecForAny())
.property("exchange_status", codecForNumber())
.property("type", codecForConstString("failure"))
.build("MerchantAbortPayRefundFailureStatus");
export const codecForMerchantAbortPayRefundStatus =
(): Codec<MerchantAbortPayRefundStatus> =>
buildCodecForUnion<MerchantAbortPayRefundStatus>()
.discriminateOn("type")
.alternative("success", codecForMerchantAbortPayRefundSuccessStatus())
.alternative("failure", codecForMerchantAbortPayRefundFailureStatus())
.build("MerchantAbortPayRefundStatus");
export const codecForAbortResponse = (): Codec<AbortResponse> => export const codecForAbortResponse = (): Codec<AbortResponse> =>
buildCodecForObject<AbortResponse>() buildCodecForObject<AbortResponse>()
.property("refunds", codecForList(codecForMerchantAbortPayRefundStatus())) .property("refunds", codecForList(codecForMerchantAbortPayRefundStatus()))
@ -1629,32 +1728,6 @@ export interface MerchantAbortPayRefundSuccessStatus {
exchange_pub: string; exchange_pub: string;
} }
export const codecForMerchantAbortPayRefundSuccessStatus =
(): Codec<MerchantAbortPayRefundSuccessStatus> =>
buildCodecForObject<MerchantAbortPayRefundSuccessStatus>()
.property("exchange_pub", codecForString())
.property("exchange_sig", codecForString())
.property("exchange_status", codecForConstNumber(200))
.property("type", codecForConstString("success"))
.build("MerchantAbortPayRefundSuccessStatus");
export const codecForMerchantAbortPayRefundFailureStatus =
(): Codec<MerchantAbortPayRefundFailureStatus> =>
buildCodecForObject<MerchantAbortPayRefundFailureStatus>()
.property("exchange_code", codecForNumber())
.property("exchange_reply", codecForAny())
.property("exchange_status", codecForNumber())
.property("type", codecForConstString("failure"))
.build("MerchantAbortPayRefundFailureStatus");
export const codecForMerchantAbortPayRefundStatus =
(): Codec<MerchantAbortPayRefundStatus> =>
buildCodecForUnion<MerchantAbortPayRefundStatus>()
.discriminateOn("type")
.alternative("success", codecForMerchantAbortPayRefundSuccessStatus())
.alternative("failure", codecForMerchantAbortPayRefundFailureStatus())
.build("MerchantAbortPayRefundStatus");
export interface TalerConfigResponse { export interface TalerConfigResponse {
name: string; name: string;
version: string; version: string;

View File

@ -51,6 +51,7 @@ import {
encryptContractForMerge, encryptContractForMerge,
ExchangeProtocolVersion, ExchangeProtocolVersion,
getRandomBytes, getRandomBytes,
GlobalFees,
hash, hash,
HashCodeString, HashCodeString,
hashCoinEv, hashCoinEv,
@ -74,6 +75,7 @@ import {
rsaVerify, rsaVerify,
setupTipPlanchet, setupTipPlanchet,
stringToBytes, stringToBytes,
TalerProtocolDuration,
TalerProtocolTimestamp, TalerProtocolTimestamp,
TalerSignaturePurpose, TalerSignaturePurpose,
UnblindedSignature, UnblindedSignature,
@ -142,6 +144,10 @@ export interface TalerCryptoInterface {
isValidWireFee(req: WireFeeValidationRequest): Promise<ValidationResult>; isValidWireFee(req: WireFeeValidationRequest): Promise<ValidationResult>;
isValidGlobalFees(
req: GlobalFeesValidationRequest,
): Promise<ValidationResult>;
isValidDenom(req: DenominationValidationRequest): Promise<ValidationResult>; isValidDenom(req: DenominationValidationRequest): Promise<ValidationResult>;
isValidWireAccount( isValidWireAccount(
@ -152,7 +158,7 @@ export interface TalerCryptoInterface {
req: ContractTermsValidationRequest, req: ContractTermsValidationRequest,
): Promise<ValidationResult>; ): Promise<ValidationResult>;
createEddsaKeypair(req: {}): Promise<EddsaKeypair>; createEddsaKeypair(req: unknown): Promise<EddsaKeypair>;
eddsaGetPublic(req: EddsaGetPublicRequest): Promise<EddsaGetPublicResponse>; eddsaGetPublic(req: EddsaGetPublicRequest): Promise<EddsaGetPublicResponse>;
@ -283,12 +289,17 @@ export const nullCrypto: TalerCryptoInterface = {
): Promise<ValidationResult> { ): Promise<ValidationResult> {
throw new Error("Function not implemented."); throw new Error("Function not implemented.");
}, },
isValidGlobalFees: function (
req: GlobalFeesValidationRequest,
): Promise<ValidationResult> {
throw new Error("Function not implemented.");
},
isValidContractTermsSignature: function ( isValidContractTermsSignature: function (
req: ContractTermsValidationRequest, req: ContractTermsValidationRequest,
): Promise<ValidationResult> { ): Promise<ValidationResult> {
throw new Error("Function not implemented."); throw new Error("Function not implemented.");
}, },
createEddsaKeypair: function (req: {}): Promise<EddsaKeypair> { createEddsaKeypair: function (req: unknown): Promise<EddsaKeypair> {
throw new Error("Function not implemented."); throw new Error("Function not implemented.");
}, },
eddsaGetPublic: function (req: EddsaGetPublicRequest): Promise<EddsaKeypair> { eddsaGetPublic: function (req: EddsaGetPublicRequest): Promise<EddsaKeypair> {
@ -484,6 +495,11 @@ export interface WireFeeValidationRequest {
masterPub: string; masterPub: string;
} }
export interface GlobalFeesValidationRequest {
gf: GlobalFees;
masterPub: string;
}
export interface DenominationValidationRequest { export interface DenominationValidationRequest {
denom: DenominationRecord; denom: DenominationRecord;
masterPub: string; masterPub: string;
@ -887,6 +903,30 @@ export const nativeCryptoR: TalerCryptoInterfaceR = {
return { valid: eddsaVerify(p, sig, pub) }; return { valid: eddsaVerify(p, sig, pub) };
}, },
/**
* Check if a global fee is correctly signed.
*/
async isValidGlobalFees(
tci: TalerCryptoInterfaceR,
req: GlobalFeesValidationRequest,
): Promise<ValidationResult> {
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. * Check if the signature of a denomination is valid.
*/ */
@ -1630,6 +1670,24 @@ function timestampRoundedToBuffer(ts: TalerProtocolTimestamp): Uint8Array {
return new Uint8Array(b); 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 { export interface EddsaSignRequest {
msg: string; msg: string;
priv: string; priv: string;

View File

@ -45,6 +45,7 @@ import {
Location, Location,
WireInfo, WireInfo,
DenominationInfo, DenominationInfo,
GlobalFees,
} from "@gnu-taler/taler-util"; } from "@gnu-taler/taler-util";
import { RetryInfo, RetryTags } from "./util/retries.js"; import { RetryInfo, RetryTags } from "./util/retries.js";
import { Event, IDBDatabase } from "@gnu-taler/idb-bridge"; import { Event, IDBDatabase } from "@gnu-taler/idb-bridge";
@ -424,6 +425,10 @@ export interface ExchangeDetailsRecord {
reserveClosingDelay: TalerProtocolDuration; reserveClosingDelay: TalerProtocolDuration;
/**
* Fees for exchange services
*/
globalFees: GlobalFees[];
/** /**
* Signing keys we got from the exchange, can also contain * Signing keys we got from the exchange, can also contain
* older signing keys that are not returned by /keys anymore. * older signing keys that are not returned by /keys anymore.

View File

@ -345,6 +345,7 @@ export async function exportBackup(
stamp_expire: x.stamp_expire, stamp_expire: x.stamp_expire,
stamp_start: x.stamp_start, stamp_start: x.stamp_start,
})), })),
global_fees: ex.globalFees,
tos_accepted_etag: ex.termsOfServiceAcceptedEtag, tos_accepted_etag: ex.termsOfServiceAcceptedEtag,
tos_accepted_timestamp: ex.termsOfServiceAcceptedTimestamp, tos_accepted_timestamp: ex.termsOfServiceAcceptedTimestamp,
denominations: denominations:

View File

@ -405,6 +405,7 @@ export async function importBackup(
masterPublicKey: backupExchangeDetails.master_public_key, masterPublicKey: backupExchangeDetails.master_public_key,
protocolVersion: backupExchangeDetails.protocol_version, protocolVersion: backupExchangeDetails.protocol_version,
reserveClosingDelay: backupExchangeDetails.reserve_closing_delay, reserveClosingDelay: backupExchangeDetails.reserve_closing_delay,
globalFees: backupExchangeDetails.global_fees,
signingKeys: backupExchangeDetails.signing_keys.map((x) => ({ signingKeys: backupExchangeDetails.signing_keys.map((x) => ({
key: x.key, key: x.key,
master_sig: x.master_sig, master_sig: x.master_sig,

View File

@ -32,6 +32,7 @@ import {
ExchangeDenomination, ExchangeDenomination,
ExchangeSignKeyJson, ExchangeSignKeyJson,
ExchangeWireJson, ExchangeWireJson,
GlobalFees,
hashDenomPub, hashDenomPub,
j2s, j2s,
LibtoolVersion, LibtoolVersion,
@ -269,6 +270,32 @@ async function validateWireInfo(
}; };
} }
async function validateGlobalFees(
ws: InternalWalletState,
fees: GlobalFees[],
masterPub: string,
): Promise<GlobalFees[]> {
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 { export interface ExchangeInfo {
wire: ExchangeWireJson; wire: ExchangeWireJson;
keys: ExchangeKeysDownloadResult; keys: ExchangeKeysDownloadResult;
@ -359,6 +386,7 @@ interface ExchangeKeysDownloadResult {
expiry: TalerProtocolTimestamp; expiry: TalerProtocolTimestamp;
recoup: Recoup[]; recoup: Recoup[];
listIssueDate: TalerProtocolTimestamp; listIssueDate: TalerProtocolTimestamp;
globalFees: GlobalFees[];
} }
/** /**
@ -432,6 +460,7 @@ async function downloadExchangeKeysInfo(
), ),
recoup: exchangeKeysJsonUnchecked.recoup ?? [], recoup: exchangeKeysJsonUnchecked.recoup ?? [],
listIssueDate: exchangeKeysJsonUnchecked.list_issue_date, listIssueDate: exchangeKeysJsonUnchecked.list_issue_date,
globalFees: exchangeKeysJsonUnchecked.global_fees,
}; };
} }
@ -552,6 +581,12 @@ export async function updateExchangeFromUrlHandler(
keysInfo.masterPublicKey, keysInfo.masterPublicKey,
); );
const globalFees = await validateGlobalFees(
ws,
keysInfo.globalFees,
keysInfo.masterPublicKey,
);
logger.info("finished validating exchange /wire info"); logger.info("finished validating exchange /wire info");
const tosDownload = await downloadTosFromAcceptedFormat( const tosDownload = await downloadTosFromAcceptedFormat(
@ -594,6 +629,7 @@ export async function updateExchangeFromUrlHandler(
protocolVersion: keysInfo.protocolVersion, protocolVersion: keysInfo.protocolVersion,
signingKeys: keysInfo.signingKeys, signingKeys: keysInfo.signingKeys,
reserveClosingDelay: keysInfo.reserveClosingDelay, reserveClosingDelay: keysInfo.reserveClosingDelay,
globalFees,
exchangeBaseUrl: r.baseUrl, exchangeBaseUrl: r.baseUrl,
wireInfo, wireInfo,
termsOfServiceText: tosDownload.tosText, termsOfServiceText: tosDownload.tosText,