download, store and check signatures for wire fees

This commit is contained in:
Florian Dold 2017-04-27 03:09:29 +02:00
parent 68e44e0e80
commit 82b5754e15
No known key found for this signature in database
GPG Key ID: D2E4F00F29D02A4B
6 changed files with 289 additions and 35 deletions

View File

@ -40,9 +40,17 @@ export namespace Checkable {
interface Prop { interface Prop {
propertyKey: any; propertyKey: any;
checker: any; checker: any;
type: any; type?: any;
elementChecker?: any; elementChecker?: any;
elementProp?: any; elementProp?: any;
keyProp?: any;
valueProp?: any;
optional?: boolean;
extraAllowed?: boolean;
}
interface CheckableInfo {
props: Prop[];
} }
export let SchemaError = (function SchemaError(message: string) { export let SchemaError = (function SchemaError(message: string) {
@ -54,7 +62,24 @@ export namespace Checkable {
SchemaError.prototype = new Error; 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 { function checkNumber(target: any, prop: Prop, path: Path): any {
@ -104,6 +129,17 @@ export namespace Checkable {
return target; 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 { function checkOptional(target: any, prop: Prop, path: Path): any {
console.assert(prop.propertyKey); console.assert(prop.propertyKey);
@ -124,7 +160,7 @@ export namespace Checkable {
throw new SchemaError( throw new SchemaError(
`expected object for ${path.join(".")}, got ${typeof v} instead`); `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 remainingPropNames = new Set(Object.getOwnPropertyNames(v));
let obj = new type(); let obj = new type();
for (let prop of props) { for (let prop of props) {
@ -132,7 +168,7 @@ export namespace Checkable {
if (prop.optional) { if (prop.optional) {
continue; continue;
} }
throw new SchemaError("Property missing: " + prop.propertyKey); throw new SchemaError(`Property ${prop.propertyKey} missing on ${path}`);
} }
if (!remainingPropNames.delete(prop.propertyKey)) { if (!remainingPropNames.delete(prop.propertyKey)) {
throw new SchemaError("assertion failed"); throw new SchemaError("assertion failed");
@ -143,7 +179,7 @@ export namespace Checkable {
path.concat([prop.propertyKey])); path.concat([prop.propertyKey]));
} }
if (remainingPropNames.size != 0) { if (!prop.extraAllowed && remainingPropNames.size != 0) {
throw new SchemaError("superfluous properties " + JSON.stringify(Array.from( throw new SchemaError("superfluous properties " + JSON.stringify(Array.from(
remainingPropNames.values()))); remainingPropNames.values())));
} }
@ -162,6 +198,18 @@ export namespace Checkable {
return target; 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) { export function ClassWithValidator(target: any) {
target.checked = (v: any) => { target.checked = (v: any) => {
@ -187,7 +235,7 @@ export namespace Checkable {
throw Error("Type does not exist yet (wrong order of definitions?)"); throw Error("Type does not exist yet (wrong order of definitions?)");
} }
function deco(target: Object, propertyKey: string | symbol): void { function deco(target: Object, propertyKey: string | symbol): void {
let chk = mkChk(target); let chk = getCheckableInfo(target);
chk.props.push({ chk.props.push({
propertyKey: propertyKey, propertyKey: propertyKey,
checker: checkValue, checker: checkValue,
@ -202,13 +250,13 @@ export namespace Checkable {
export function List(type: any) { export function List(type: any) {
let stub = {}; let stub = {};
type(stub, "(list-element)"); type(stub, "(list-element)");
let elementProp = mkChk(stub).props[0]; let elementProp = getCheckableInfo(stub).props[0];
let elementChecker = elementProp.checker; let elementChecker = elementProp.checker;
if (!elementChecker) { if (!elementChecker) {
throw Error("assertion failed"); throw Error("assertion failed");
} }
function deco(target: Object, propertyKey: string | symbol): void { function deco(target: Object, propertyKey: string | symbol): void {
let chk = mkChk(target); let chk = getCheckableInfo(target);
chk.props.push({ chk.props.push({
elementChecker, elementChecker,
elementProp, 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) { export function Optional(type: any) {
let stub = {}; let stub = {};
type(stub, "(optional-element)"); type(stub, "(optional-element)");
let elementProp = mkChk(stub).props[0]; let elementProp = getCheckableInfo(stub).props[0];
let elementChecker = elementProp.checker; let elementChecker = elementProp.checker;
if (!elementChecker) { if (!elementChecker) {
throw Error("assertion failed"); throw Error("assertion failed");
} }
function deco(target: Object, propertyKey: string | symbol): void { function deco(target: Object, propertyKey: string | symbol): void {
let chk = mkChk(target); let chk = getCheckableInfo(target);
chk.props.push({ chk.props.push({
elementChecker, elementChecker,
elementProp, elementProp,
@ -245,14 +320,13 @@ export namespace Checkable {
export function Number(target: Object, propertyKey: string | symbol): void { export function Number(target: Object, propertyKey: string | symbol): void {
let chk = mkChk(target); let chk = getCheckableInfo(target);
chk.props.push({ propertyKey: propertyKey, checker: checkNumber }); chk.props.push({ propertyKey: propertyKey, checker: checkNumber });
} }
export function AnyObject(target: Object, export function AnyObject(target: Object, propertyKey: string | symbol): void {
propertyKey: string | symbol): void { let chk = getCheckableInfo(target);
let chk = mkChk(target);
chk.props.push({ chk.props.push({
propertyKey: propertyKey, propertyKey: propertyKey,
checker: checkAnyObject checker: checkAnyObject
@ -260,9 +334,8 @@ export namespace Checkable {
} }
export function Any(target: Object, export function Any(target: Object, propertyKey: string | symbol): void {
propertyKey: string | symbol): void { let chk = getCheckableInfo(target);
let chk = mkChk(target);
chk.props.push({ chk.props.push({
propertyKey: propertyKey, propertyKey: propertyKey,
checker: checkAny, checker: checkAny,
@ -272,22 +345,14 @@ export namespace Checkable {
export function String(target: Object, propertyKey: string | symbol): void { export function String(target: Object, propertyKey: string | symbol): void {
let chk = mkChk(target); let chk = getCheckableInfo(target);
chk.props.push({ propertyKey: propertyKey, checker: checkString }); chk.props.push({ propertyKey: propertyKey, checker: checkString });
} }
export function Boolean(target: Object, propertyKey: string | symbol): void { export function Boolean(target: Object, propertyKey: string | symbol): void {
let chk = mkChk(target); let chk = getCheckableInfo(target);
chk.props.push({ propertyKey: propertyKey, checker: checkBoolean }); chk.props.push({ propertyKey: propertyKey, checker: checkBoolean });
} }
function mkChk(target: any) {
let chk = target[chkSym];
if (!chk) {
chk = { props: [] };
target[chkSym] = chk;
}
return chk;
}
} }

View File

@ -28,7 +28,7 @@ import {
import {OfferRecord} from "./wallet"; import {OfferRecord} from "./wallet";
import {CoinWithDenom} from "./wallet"; import {CoinWithDenom} from "./wallet";
import {PayCoinInfo} from "./types"; import {PayCoinInfo} from "./types";
import {RefreshSessionRecord} from "./types"; import {RefreshSessionRecord, WireFee} from "./types";
interface WorkerState { interface WorkerState {
@ -235,6 +235,10 @@ export class CryptoApi {
return this.doRpc<boolean>("isValidDenom", 2, denom, masterPub); 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) { isValidPaymentSignature(sig: string, contractHash: string, merchantPub: string) {
return this.doRpc<PayCoinInfo>("isValidPaymentSignature", 1, sig, contractHash, merchantPub); return this.doRpc<PayCoinInfo>("isValidPaymentSignature", 1, sig, contractHash, merchantPub);
} }

View File

@ -30,7 +30,7 @@ import create = chrome.alarms.create;
import {OfferRecord} from "./wallet"; import {OfferRecord} from "./wallet";
import {CoinWithDenom} from "./wallet"; import {CoinWithDenom} from "./wallet";
import {CoinPaySig, CoinRecord} from "./types"; import {CoinPaySig, CoinRecord} from "./types";
import {DenominationRecord, Amounts} from "./types"; import {DenominationRecord, Amounts, WireFee} from "./types";
import {Amount} from "./emscriptif"; import {Amount} from "./emscriptif";
import {HashContext} from "./emscriptif"; import {HashContext} from "./emscriptif";
import {RefreshMeltCoinAffirmationPS} from "./emscriptif"; import {RefreshMeltCoinAffirmationPS} from "./emscriptif";
@ -110,6 +110,25 @@ namespace RpcFunctions {
nativePub); 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, export function isValidDenom(denom: DenominationRecord,
masterPub: string): boolean { masterPub: string): boolean {

View File

@ -207,6 +207,7 @@ export enum SignaturePurpose {
WALLET_COIN_MELT = 1202, WALLET_COIN_MELT = 1202,
TEST = 4242, TEST = 4242,
MERCHANT_PAYMENT_OK = 1104, 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 { export class AbsoluteTimeNbo extends PackedArenaObject {
static fromTalerString(s: string): AbsoluteTimeNbo { static fromTalerString(s: string): AbsoluteTimeNbo {
let x = new AbsoluteTimeNbo(); let x = new AbsoluteTimeNbo();
@ -1008,6 +1038,15 @@ export class AbsoluteTimeNbo extends PackedArenaObject {
return x; 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() { size() {
return 8; return 8;
} }

View File

@ -474,6 +474,9 @@ export class Contract {
@Checkable.String @Checkable.String
H_wire: string; H_wire: string;
@Checkable.String
wire_method: string;
@Checkable.Optional(Checkable.String) @Checkable.Optional(Checkable.String)
summary?: 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 }>; export type PayCoinInfo = Array<{ updatedCoin: CoinRecord, sig: CoinPaySig }>;

View File

@ -42,6 +42,8 @@ import {
AuditorRecord, AuditorRecord,
WalletBalance, WalletBalance,
WalletBalanceEntry, WalletBalanceEntry,
WireFee,
ExchangeWireFeesRecord,
WireInfo, DenominationRecord, DenominationStatus, denominationRecordFromKeys, WireInfo, DenominationRecord, DenominationStatus, denominationRecordFromKeys,
CoinStatus, CoinStatus,
} from "./types"; } 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 @Checkable.Class
export class CreateReserveRequest { export class CreateReserveRequest {
/** /**
@ -223,6 +260,7 @@ export interface ConfigRecord {
} }
const builtinCurrencies: CurrencyRecord[] = [ const builtinCurrencies: CurrencyRecord[] = [
{ {
name: "KUDOS", 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 exchanges: ExchangeStore = new ExchangeStore();
export const exchangeWireFees: ExchangeWireFeesStore = new ExchangeWireFeesStore();
export const nonces: NonceStore = new NonceStore(); export const nonces: NonceStore = new NonceStore();
export const transactions: TransactionsStore = new TransactionsStore(); export const transactions: TransactionsStore = new TransactionsStore();
export const reserves: Store<ReserveRecord> = new Store<ReserveRecord>("reserves", {keyPath: "reserve_pub"}); 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> { async updateExchangeFromUrl(baseUrl: string): Promise<ExchangeRecord> {
baseUrl = canonicalizeBaseUrl(baseUrl); baseUrl = canonicalizeBaseUrl(baseUrl);
let reqUrl = new URI("keys").absoluteTo(baseUrl); let keysUrl = new URI("keys").absoluteTo(baseUrl);
let resp = await this.http.get(reqUrl.href()); let wireUrl = new URI("wire").absoluteTo(baseUrl);
if (resp.status != 200) { let keysResp = await this.http.get(keysUrl.href());
if (keysResp.status != 200) {
throw Error("/keys request failed"); throw Error("/keys request failed");
} }
let exchangeKeysJson = KeysJson.checked(JSON.parse(resp.responseText)); let wireResp = await this.http.get(wireUrl.href());
return this.updateExchangeFromJson(baseUrl, exchangeKeysJson); 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, 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); const updateTimeSec = getTalerStampSec(exchangeKeysJson.list_issue_date);
if (updateTimeSec === null) { if (updateTimeSec === null) {
throw Error("invalid update time"); throw Error("invalid update time");
@ -1325,6 +1386,55 @@ export class Wallet {
.put(Stores.exchanges, updatedExchangeInfo) .put(Stores.exchanges, updatedExchangeInfo)
.finish(); .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; return updatedExchangeInfo;
} }