From 82b5754e157a1a3b22afe48c8366c76525eb91e3 Mon Sep 17 00:00:00 2001 From: Florian Dold Date: Thu, 27 Apr 2017 03:09:29 +0200 Subject: download, store and check signatures for wire fees --- src/checkable.ts | 119 ++++++++++++++++++++++++++++++++++++++------------ src/cryptoApi.ts | 6 ++- src/cryptoWorker.ts | 21 ++++++++- src/emscriptif.ts | 39 +++++++++++++++++ src/types.ts | 17 ++++++++ src/wallet.ts | 122 +++++++++++++++++++++++++++++++++++++++++++++++++--- 6 files changed, 289 insertions(+), 35 deletions(-) diff --git a/src/checkable.ts b/src/checkable.ts index c8cc27b26..8af70f50f 100644 --- a/src/checkable.ts +++ b/src/checkable.ts @@ -40,9 +40,17 @@ export namespace Checkable { interface Prop { propertyKey: any; checker: any; - type: any; + type?: any; elementChecker?: any; elementProp?: any; + keyProp?: any; + valueProp?: any; + optional?: boolean; + extraAllowed?: boolean; + } + + interface CheckableInfo { + props: Prop[]; } export let SchemaError = (function SchemaError(message: string) { @@ -54,7 +62,24 @@ export namespace Checkable { SchemaError.prototype = new Error; - let chkSym = Symbol("checkable"); + /** + * Classes that are checkable are annotated with this + * checkable info symbol, which contains the information necessary + * to check if they're valid. + */ + let checkableInfoSym = Symbol("checkableInfo"); + + /** + * Get the current property list for a checkable type. + */ + function getCheckableInfo(target: any): CheckableInfo { + let chk = target[checkableInfoSym] as CheckableInfo|undefined; + if (!chk) { + chk = { props: [] }; + target[checkableInfoSym] = chk; + } + return chk; + } function checkNumber(target: any, prop: Prop, path: Path): any { @@ -104,6 +129,17 @@ export namespace Checkable { return target; } + function checkMap(target: any, prop: Prop, path: Path): any { + if (typeof target !== "object") { + throw new SchemaError(`expected object for ${path}, got ${typeof target} instead`); + } + for (let key in target) { + prop.keyProp.checker(key, prop.keyProp, path.concat([key])); + let value = target[key]; + prop.valueProp.checker(value, prop.valueProp, path.concat([key])); + } + } + function checkOptional(target: any, prop: Prop, path: Path): any { console.assert(prop.propertyKey); @@ -124,7 +160,7 @@ export namespace Checkable { throw new SchemaError( `expected object for ${path.join(".")}, got ${typeof v} instead`); } - let props = type.prototype[chkSym].props; + let props = type.prototype[checkableInfoSym].props; let remainingPropNames = new Set(Object.getOwnPropertyNames(v)); let obj = new type(); for (let prop of props) { @@ -132,7 +168,7 @@ export namespace Checkable { if (prop.optional) { continue; } - throw new SchemaError("Property missing: " + prop.propertyKey); + throw new SchemaError(`Property ${prop.propertyKey} missing on ${path}`); } if (!remainingPropNames.delete(prop.propertyKey)) { throw new SchemaError("assertion failed"); @@ -143,7 +179,7 @@ export namespace Checkable { path.concat([prop.propertyKey])); } - if (remainingPropNames.size != 0) { + if (!prop.extraAllowed && remainingPropNames.size != 0) { throw new SchemaError("superfluous properties " + JSON.stringify(Array.from( remainingPropNames.values()))); } @@ -162,6 +198,18 @@ export namespace Checkable { return target; } + export function ClassWithExtra(target: any) { + target.checked = (v: any) => { + return checkValue(v, { + propertyKey: "(root)", + type: target, + extraAllowed: true, + checker: checkValue + }, ["(root)"]); + }; + return target; + } + export function ClassWithValidator(target: any) { target.checked = (v: any) => { @@ -187,7 +235,7 @@ export namespace Checkable { throw Error("Type does not exist yet (wrong order of definitions?)"); } function deco(target: Object, propertyKey: string | symbol): void { - let chk = mkChk(target); + let chk = getCheckableInfo(target); chk.props.push({ propertyKey: propertyKey, checker: checkValue, @@ -202,13 +250,13 @@ export namespace Checkable { export function List(type: any) { let stub = {}; type(stub, "(list-element)"); - let elementProp = mkChk(stub).props[0]; + let elementProp = getCheckableInfo(stub).props[0]; let elementChecker = elementProp.checker; if (!elementChecker) { throw Error("assertion failed"); } function deco(target: Object, propertyKey: string | symbol): void { - let chk = mkChk(target); + let chk = getCheckableInfo(target); chk.props.push({ elementChecker, elementProp, @@ -221,16 +269,43 @@ export namespace Checkable { } + export function Map(keyType: any, valueType: any) { + let keyStub = {}; + keyType(keyStub, "(map-key)"); + let keyProp = getCheckableInfo(keyStub).props[0]; + if (!keyProp) { + throw Error("assertion failed"); + } + let valueStub = {}; + valueType(valueStub, "(map-value)"); + let valueProp = getCheckableInfo(valueStub).props[0]; + if (!valueProp) { + throw Error("assertion failed"); + } + function deco(target: Object, propertyKey: string | symbol): void { + let chk = getCheckableInfo(target); + chk.props.push({ + keyProp, + valueProp, + propertyKey: propertyKey, + checker: checkMap, + }); + } + + return deco; + } + + export function Optional(type: any) { let stub = {}; type(stub, "(optional-element)"); - let elementProp = mkChk(stub).props[0]; + let elementProp = getCheckableInfo(stub).props[0]; let elementChecker = elementProp.checker; if (!elementChecker) { throw Error("assertion failed"); } function deco(target: Object, propertyKey: string | symbol): void { - let chk = mkChk(target); + let chk = getCheckableInfo(target); chk.props.push({ elementChecker, elementProp, @@ -245,14 +320,13 @@ export namespace Checkable { export function Number(target: Object, propertyKey: string | symbol): void { - let chk = mkChk(target); + let chk = getCheckableInfo(target); chk.props.push({ propertyKey: propertyKey, checker: checkNumber }); } - export function AnyObject(target: Object, - propertyKey: string | symbol): void { - let chk = mkChk(target); + export function AnyObject(target: Object, propertyKey: string | symbol): void { + let chk = getCheckableInfo(target); chk.props.push({ propertyKey: propertyKey, checker: checkAnyObject @@ -260,9 +334,8 @@ export namespace Checkable { } - export function Any(target: Object, - propertyKey: string | symbol): void { - let chk = mkChk(target); + export function Any(target: Object, propertyKey: string | symbol): void { + let chk = getCheckableInfo(target); chk.props.push({ propertyKey: propertyKey, checker: checkAny, @@ -272,22 +345,14 @@ export namespace Checkable { export function String(target: Object, propertyKey: string | symbol): void { - let chk = mkChk(target); + let chk = getCheckableInfo(target); chk.props.push({ propertyKey: propertyKey, checker: checkString }); } export function Boolean(target: Object, propertyKey: string | symbol): void { - let chk = mkChk(target); + let chk = getCheckableInfo(target); chk.props.push({ propertyKey: propertyKey, checker: checkBoolean }); } - function mkChk(target: any) { - let chk = target[chkSym]; - if (!chk) { - chk = { props: [] }; - target[chkSym] = chk; - } - return chk; - } } diff --git a/src/cryptoApi.ts b/src/cryptoApi.ts index 98fc2c66a..5657d74d6 100644 --- a/src/cryptoApi.ts +++ b/src/cryptoApi.ts @@ -28,7 +28,7 @@ import { import {OfferRecord} from "./wallet"; import {CoinWithDenom} from "./wallet"; import {PayCoinInfo} from "./types"; -import {RefreshSessionRecord} from "./types"; +import {RefreshSessionRecord, WireFee} from "./types"; interface WorkerState { @@ -235,6 +235,10 @@ export class CryptoApi { return this.doRpc("isValidDenom", 2, denom, masterPub); } + isValidWireFee(type: string, wf: WireFee, masterPub: string): Promise { + return this.doRpc("isValidWireFee", 2, type, wf, masterPub); + } + isValidPaymentSignature(sig: string, contractHash: string, merchantPub: string) { return this.doRpc("isValidPaymentSignature", 1, sig, contractHash, merchantPub); } diff --git a/src/cryptoWorker.ts b/src/cryptoWorker.ts index cb7bee40b..4275d659b 100644 --- a/src/cryptoWorker.ts +++ b/src/cryptoWorker.ts @@ -30,7 +30,7 @@ import create = chrome.alarms.create; import {OfferRecord} from "./wallet"; import {CoinWithDenom} from "./wallet"; import {CoinPaySig, CoinRecord} from "./types"; -import {DenominationRecord, Amounts} from "./types"; +import {DenominationRecord, Amounts, WireFee} from "./types"; import {Amount} from "./emscriptif"; import {HashContext} from "./emscriptif"; import {RefreshMeltCoinAffirmationPS} from "./emscriptif"; @@ -110,6 +110,25 @@ namespace RpcFunctions { nativePub); } + export function isValidWireFee(type: string, wf: WireFee, masterPub: string): boolean { + let p = new native.MasterWireFeePS({ + h_wire_method: native.ByteArray.fromStringWithNull(type).hash(), + start_date: native.AbsoluteTimeNbo.fromStamp(wf.startStamp), + end_date: native.AbsoluteTimeNbo.fromStamp(wf.endStamp), + wire_fee: (new native.Amount(wf.wireFee)).toNbo(), + closing_fee: (new native.Amount(wf.closingFee)).toNbo(), + }); + + let nativeSig = new native.EddsaSignature(); + nativeSig.loadCrock(wf.sig); + let nativePub = native.EddsaPublicKey.fromCrock(masterPub); + + return native.eddsaVerify(native.SignaturePurpose.MASTER_WIRE_FEES, + p.toPurpose(), + nativeSig, + nativePub); + } + export function isValidDenom(denom: DenominationRecord, masterPub: string): boolean { diff --git a/src/emscriptif.ts b/src/emscriptif.ts index 3a34f6451..3f23476aa 100644 --- a/src/emscriptif.ts +++ b/src/emscriptif.ts @@ -207,6 +207,7 @@ export enum SignaturePurpose { WALLET_COIN_MELT = 1202, TEST = 4242, MERCHANT_PAYMENT_OK = 1104, + MASTER_WIRE_FEES = 1028, } @@ -993,6 +994,35 @@ export class RefreshMeltCoinAffirmationPS extends SignatureStruct { } +interface MasterWireFeePS_Args { + h_wire_method: HashCode; + start_date: AbsoluteTimeNbo; + end_date: AbsoluteTimeNbo; + wire_fee: AmountNbo; + closing_fee: AmountNbo; +} + +export class MasterWireFeePS extends SignatureStruct { + constructor(w: MasterWireFeePS_Args) { + super(w); + } + + purpose() { + return SignaturePurpose.MASTER_WIRE_FEES; + } + + fieldTypes() { + return [ + ["h_wire_method", HashCode], + ["start_date", AbsoluteTimeNbo], + ["end_date", AbsoluteTimeNbo], + ["wire_fee", AmountNbo], + ["closing_fee", AmountNbo], + ]; + } +} + + export class AbsoluteTimeNbo extends PackedArenaObject { static fromTalerString(s: string): AbsoluteTimeNbo { let x = new AbsoluteTimeNbo(); @@ -1008,6 +1038,15 @@ export class AbsoluteTimeNbo extends PackedArenaObject { return x; } + static fromStamp(stamp: number): AbsoluteTimeNbo { + let x = new AbsoluteTimeNbo(); + x.alloc(); + // XXX: This only works up to 54 bit numbers. + set64(x.nativePtr, stamp); + return x; + } + + size() { return 8; } diff --git a/src/types.ts b/src/types.ts index e0baa169b..c6111bd09 100644 --- a/src/types.ts +++ b/src/types.ts @@ -474,6 +474,9 @@ export class Contract { @Checkable.String H_wire: string; + @Checkable.String + wire_method: string; + @Checkable.Optional(Checkable.String) summary?: string; @@ -535,6 +538,20 @@ export class Contract { } +export interface WireFee { + wireFee: AmountJson; + closingFee: AmountJson; + startStamp: number; + endStamp: number; + sig: string; +} + +export interface ExchangeWireFeesRecord { + exchangeBaseUrl: string; + feesForType: { [type: string]: WireFee[] }; +} + + export type PayCoinInfo = Array<{ updatedCoin: CoinRecord, sig: CoinPaySig }>; diff --git a/src/wallet.ts b/src/wallet.ts index e8b655ffd..8b220ec4f 100644 --- a/src/wallet.ts +++ b/src/wallet.ts @@ -42,6 +42,8 @@ import { AuditorRecord, WalletBalance, WalletBalanceEntry, + WireFee, + ExchangeWireFeesRecord, WireInfo, DenominationRecord, DenominationStatus, denominationRecordFromKeys, CoinStatus, } from "./types"; @@ -113,6 +115,41 @@ export class KeysJson { } + + +@Checkable.Class +class WireFeesJson { + @Checkable.Value(AmountJson) + wire_fee: AmountJson; + + @Checkable.Value(AmountJson) + closing_fee: AmountJson; + + @Checkable.String + sig: string; + + @Checkable.String + start_date: string; + + @Checkable.String + end_date: string; + + static checked: (obj: any) => WireFeesJson; +} + + +@Checkable.ClassWithExtra +class WireDetailJson { + @Checkable.String + type: string; + + @Checkable.List(Checkable.Value(WireFeesJson)) + fees: WireFeesJson[]; + + static checked: (obj: any) => WireDetailJson; +} + + @Checkable.Class export class CreateReserveRequest { /** @@ -223,6 +260,7 @@ export interface ConfigRecord { } + const builtinCurrencies: CurrencyRecord[] = [ { name: "KUDOS", @@ -417,7 +455,13 @@ export namespace Stores { } } + class ExchangeWireFeesStore extends Store { + constructor() { + super("exchangeWireFees", {keyPath: "exchangeBaseUrl"}); + } + } export const exchanges: ExchangeStore = new ExchangeStore(); + export const exchangeWireFees: ExchangeWireFeesStore = new ExchangeWireFeesStore(); export const nonces: NonceStore = new NonceStore(); export const transactions: TransactionsStore = new TransactionsStore(); export const reserves: Store = new Store("reserves", {keyPath: "reserve_pub"}); @@ -1254,13 +1298,27 @@ export class Wallet { */ async updateExchangeFromUrl(baseUrl: string): Promise { baseUrl = canonicalizeBaseUrl(baseUrl); - let reqUrl = new URI("keys").absoluteTo(baseUrl); - let resp = await this.http.get(reqUrl.href()); - if (resp.status != 200) { + let keysUrl = new URI("keys").absoluteTo(baseUrl); + let wireUrl = new URI("wire").absoluteTo(baseUrl); + let keysResp = await this.http.get(keysUrl.href()); + if (keysResp.status != 200) { throw Error("/keys request failed"); } - let exchangeKeysJson = KeysJson.checked(JSON.parse(resp.responseText)); - return this.updateExchangeFromJson(baseUrl, exchangeKeysJson); + let wireResp = await this.http.get(wireUrl.href()); + if (wireResp.status != 200) { + throw Error("/wire request failed"); + } + let exchangeKeysJson = KeysJson.checked(JSON.parse(keysResp.responseText)); + let wireRespJson = JSON.parse(wireResp.responseText); + if (typeof wireRespJson !== "object") { + throw Error("/wire response is not an object"); + } + console.log("exchange wire", wireRespJson); + let wireMethodDetails: WireDetailJson[] = []; + for (let methodName in wireRespJson) { + wireMethodDetails.push(WireDetailJson.checked(wireRespJson[methodName])); + } + return this.updateExchangeFromJson(baseUrl, exchangeKeysJson, wireMethodDetails); } @@ -1289,7 +1347,10 @@ export class Wallet { private async updateExchangeFromJson(baseUrl: string, - exchangeKeysJson: KeysJson): Promise { + exchangeKeysJson: KeysJson, + wireMethodDetails: WireDetailJson[]): Promise { + + // FIXME: all this should probably be commited atomically const updateTimeSec = getTalerStampSec(exchangeKeysJson.list_issue_date); if (updateTimeSec === null) { throw Error("invalid update time"); @@ -1325,6 +1386,55 @@ export class Wallet { .put(Stores.exchanges, updatedExchangeInfo) .finish(); + let oldWireFees = await this.q().get(Stores.exchangeWireFees, baseUrl); + if (!oldWireFees) { + oldWireFees = { + exchangeBaseUrl: baseUrl, + feesForType: {}, + }; + } + + for (let detail of wireMethodDetails) { + let latestFeeStamp = 0; + let fees = oldWireFees.feesForType[detail.type] || []; + oldWireFees.feesForType[detail.type] = fees; + for (let oldFee of fees) { + if (oldFee.endStamp > latestFeeStamp) { + latestFeeStamp = oldFee.endStamp; + } + } + for (let fee of detail.fees) { + let start = getTalerStampSec(fee.start_date); + if (start == null) { + console.error("invalid start stamp in fee", fee); + continue; + } + if (start < latestFeeStamp) { + continue; + } + let end = getTalerStampSec(fee.end_date); + if (end == null) { + console.error("invalid end stamp in fee", fee); + continue; + } + let wf: WireFee = { + wireFee: fee.wire_fee, + closingFee: fee.closing_fee, + sig: fee.sig, + startStamp: start, + endStamp: end, + } + let valid: boolean = await this.cryptoApi.isValidWireFee(detail.type, wf, exchangeInfo.masterPublicKey); + if (!valid) { + console.error("fee signature invalid", fee); + continue; + } + fees.push(wf); + } + } + + await this.q().put(Stores.exchangeWireFees, oldWireFees); + return updatedExchangeInfo; } -- cgit v1.2.3