aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorFlorian Dold <florian.dold@gmail.com>2017-04-27 03:09:29 +0200
committerFlorian Dold <florian.dold@gmail.com>2017-04-27 03:09:29 +0200
commit82b5754e157a1a3b22afe48c8366c76525eb91e3 (patch)
treec50afc6bac7535481a73ebc29049e393444f7edc
parent68e44e0e80635b00333ef2fcbb0ad937581344ba (diff)
download, store and check signatures for wire fees
-rw-r--r--src/checkable.ts119
-rw-r--r--src/cryptoApi.ts6
-rw-r--r--src/cryptoWorker.ts21
-rw-r--r--src/emscriptif.ts39
-rw-r--r--src/types.ts17
-rw-r--r--src/wallet.ts122
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<boolean>("isValidDenom", 2, denom, masterPub);
}
+ isValidWireFee(type: string, wf: WireFee, masterPub: string): Promise<boolean> {
+ return this.doRpc<boolean>("isValidWireFee", 2, type, wf, masterPub);
+ }
+
isValidPaymentSignature(sig: string, contractHash: string, merchantPub: string) {
return this.doRpc<PayCoinInfo>("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<ExchangeWireFeesRecord> {
+ 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<ReserveRecord> = new Store<ReserveRecord>("reserves", {keyPath: "reserve_pub"});
@@ -1254,13 +1298,27 @@ export class Wallet {
*/
async updateExchangeFromUrl(baseUrl: string): Promise<ExchangeRecord> {
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<ExchangeRecord> {
+ exchangeKeysJson: KeysJson,
+ wireMethodDetails: WireDetailJson[]): Promise<ExchangeRecord> {
+
+ // 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;
}