signature verification for recoup
This commit is contained in:
parent
51eef5419a
commit
1744b1a800
@ -34,7 +34,13 @@ import {
|
|||||||
|
|
||||||
import { CryptoWorker } from "./cryptoWorker";
|
import { CryptoWorker } from "./cryptoWorker";
|
||||||
|
|
||||||
import { RecoupRequest, CoinDepositPermission } from "../../types/talerTypes";
|
import {
|
||||||
|
RecoupRequest,
|
||||||
|
CoinDepositPermission,
|
||||||
|
RecoupConfirmation,
|
||||||
|
ExchangeSignKeyJson,
|
||||||
|
EddsaPublicKeyString,
|
||||||
|
} from "../../types/talerTypes";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
BenchmarkResult,
|
BenchmarkResult,
|
||||||
@ -382,13 +388,30 @@ export class CryptoApi {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate the signature in a recoup confirmation.
|
||||||
|
*/
|
||||||
|
isValidRecoupConfirmation(
|
||||||
|
recoupCoinPub: EddsaPublicKeyString,
|
||||||
|
recoupConfirmation: RecoupConfirmation,
|
||||||
|
exchangeSigningKeys: ExchangeSignKeyJson[],
|
||||||
|
): Promise<boolean> {
|
||||||
|
return this.doRpc<boolean>(
|
||||||
|
"isValidRecoupConfirmation",
|
||||||
|
1,
|
||||||
|
recoupCoinPub,
|
||||||
|
recoupConfirmation,
|
||||||
|
exchangeSigningKeys,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
signDepositPermission(
|
signDepositPermission(
|
||||||
depositInfo: DepositInfo
|
depositInfo: DepositInfo,
|
||||||
): Promise<CoinDepositPermission> {
|
): Promise<CoinDepositPermission> {
|
||||||
return this.doRpc<CoinDepositPermission>(
|
return this.doRpc<CoinDepositPermission>(
|
||||||
"signDepositPermission",
|
"signDepositPermission",
|
||||||
3,
|
3,
|
||||||
depositInfo
|
depositInfo,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -404,8 +427,18 @@ export class CryptoApi {
|
|||||||
return this.doRpc<boolean>("rsaVerify", 4, hm, sig, pk);
|
return this.doRpc<boolean>("rsaVerify", 4, hm, sig, pk);
|
||||||
}
|
}
|
||||||
|
|
||||||
isValidWireAccount(paytoUri: string, sig: string, masterPub: string): Promise<boolean> {
|
isValidWireAccount(
|
||||||
return this.doRpc<boolean>("isValidWireAccount", 4, paytoUri, sig, masterPub);
|
paytoUri: string,
|
||||||
|
sig: string,
|
||||||
|
masterPub: string,
|
||||||
|
): Promise<boolean> {
|
||||||
|
return this.doRpc<boolean>(
|
||||||
|
"isValidWireAccount",
|
||||||
|
4,
|
||||||
|
paytoUri,
|
||||||
|
sig,
|
||||||
|
masterPub,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
createRecoupRequest(coin: CoinRecord): Promise<RecoupRequest> {
|
createRecoupRequest(coin: CoinRecord): Promise<RecoupRequest> {
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
/*
|
/*
|
||||||
This file is part of GNU Taler
|
This file is part of GNU Taler
|
||||||
(C) 2019 GNUnet e.V.
|
(C) 2019-2020 Taler Systems SA
|
||||||
|
|
||||||
TALER is free software; you can redistribute it and/or modify it under the
|
TALER is free software; you can redistribute it and/or modify it under the
|
||||||
terms of the GNU General Public License as published by the Free Software
|
terms of the GNU General Public License as published by the Free Software
|
||||||
@ -18,6 +18,8 @@
|
|||||||
* Synchronous implementation of crypto-related functions for the wallet.
|
* Synchronous implementation of crypto-related functions for the wallet.
|
||||||
*
|
*
|
||||||
* The functionality is parameterized over an Emscripten environment.
|
* The functionality is parameterized over an Emscripten environment.
|
||||||
|
*
|
||||||
|
* @author Florian Dold <dold@taler.net>
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -34,7 +36,13 @@ import {
|
|||||||
CoinSourceType,
|
CoinSourceType,
|
||||||
} from "../../types/dbTypes";
|
} from "../../types/dbTypes";
|
||||||
|
|
||||||
import { CoinDepositPermission, RecoupRequest } from "../../types/talerTypes";
|
import {
|
||||||
|
CoinDepositPermission,
|
||||||
|
RecoupRequest,
|
||||||
|
RecoupConfirmation,
|
||||||
|
ExchangeSignKeyJson,
|
||||||
|
EddsaPublicKeyString,
|
||||||
|
} from "../../types/talerTypes";
|
||||||
import {
|
import {
|
||||||
BenchmarkResult,
|
BenchmarkResult,
|
||||||
PlanchetCreationResult,
|
PlanchetCreationResult,
|
||||||
@ -63,7 +71,11 @@ import {
|
|||||||
} from "../talerCrypto";
|
} from "../talerCrypto";
|
||||||
import { randomBytes } from "../primitives/nacl-fast";
|
import { randomBytes } from "../primitives/nacl-fast";
|
||||||
import { kdf } from "../primitives/kdf";
|
import { kdf } from "../primitives/kdf";
|
||||||
import { Timestamp, getTimestampNow } from "../../util/time";
|
import {
|
||||||
|
Timestamp,
|
||||||
|
getTimestampNow,
|
||||||
|
timestampIsBetween,
|
||||||
|
} from "../../util/time";
|
||||||
|
|
||||||
enum SignaturePurpose {
|
enum SignaturePurpose {
|
||||||
RESERVE_WITHDRAW = 1200,
|
RESERVE_WITHDRAW = 1200,
|
||||||
@ -76,6 +88,8 @@ enum SignaturePurpose {
|
|||||||
MERCHANT_PAYMENT_OK = 1104,
|
MERCHANT_PAYMENT_OK = 1104,
|
||||||
WALLET_COIN_RECOUP = 1203,
|
WALLET_COIN_RECOUP = 1203,
|
||||||
WALLET_COIN_LINK = 1204,
|
WALLET_COIN_LINK = 1204,
|
||||||
|
EXCHANGE_CONFIRM_RECOUP = 1039,
|
||||||
|
EXCHANGE_CONFIRM_RECOUP_REFRESH = 1041,
|
||||||
}
|
}
|
||||||
|
|
||||||
function amountToBuffer(amount: AmountJson): Uint8Array {
|
function amountToBuffer(amount: AmountJson): Uint8Array {
|
||||||
@ -131,6 +145,19 @@ function buildSigPS(purposeNum: number): SignaturePurposeBuilder {
|
|||||||
return new SignaturePurposeBuilder(purposeNum);
|
return new SignaturePurposeBuilder(purposeNum);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function checkSignKeyOkay(
|
||||||
|
key: string,
|
||||||
|
exchangeKeys: ExchangeSignKeyJson[],
|
||||||
|
): boolean {
|
||||||
|
const now = getTimestampNow();
|
||||||
|
for (const k of exchangeKeys) {
|
||||||
|
if (k.key == key) {
|
||||||
|
return timestampIsBetween(now, k.stamp_start, k.stamp_end);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
export class CryptoImplementation {
|
export class CryptoImplementation {
|
||||||
static enableTracing: boolean = false;
|
static enableTracing: boolean = false;
|
||||||
|
|
||||||
@ -216,7 +243,7 @@ export class CryptoImplementation {
|
|||||||
coin_sig: encodeCrock(coinSig),
|
coin_sig: encodeCrock(coinSig),
|
||||||
denom_pub_hash: coin.denomPubHash,
|
denom_pub_hash: coin.denomPubHash,
|
||||||
denom_sig: coin.denomSig,
|
denom_sig: coin.denomSig,
|
||||||
refreshed: (coin.coinSource.type === CoinSourceType.Refresh),
|
refreshed: coin.coinSource.type === CoinSourceType.Refresh,
|
||||||
};
|
};
|
||||||
return paybackRequest;
|
return paybackRequest;
|
||||||
}
|
}
|
||||||
@ -327,7 +354,6 @@ export class CryptoImplementation {
|
|||||||
* and deposit permissions for each given coin.
|
* and deposit permissions for each given coin.
|
||||||
*/
|
*/
|
||||||
signDepositPermission(depositInfo: DepositInfo): CoinDepositPermission {
|
signDepositPermission(depositInfo: DepositInfo): CoinDepositPermission {
|
||||||
|
|
||||||
const d = buildSigPS(SignaturePurpose.WALLET_COIN_DEPOSIT)
|
const d = buildSigPS(SignaturePurpose.WALLET_COIN_DEPOSIT)
|
||||||
.put(decodeCrock(depositInfo.contractTermsHash))
|
.put(decodeCrock(depositInfo.contractTermsHash))
|
||||||
.put(decodeCrock(depositInfo.wireInfoHash))
|
.put(decodeCrock(depositInfo.wireInfoHash))
|
||||||
@ -492,6 +518,44 @@ export class CryptoImplementation {
|
|||||||
return encodeCrock(sig);
|
return encodeCrock(sig);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate the signature in a recoup confirmation.
|
||||||
|
*/
|
||||||
|
isValidRecoupConfirmation(
|
||||||
|
recoupCoinPub: EddsaPublicKeyString,
|
||||||
|
recoupConfirmation: RecoupConfirmation,
|
||||||
|
exchangeSigningKeys: ExchangeSignKeyJson[],
|
||||||
|
): boolean {
|
||||||
|
const pubEnc = recoupConfirmation.exchange_pub;
|
||||||
|
if (!checkSignKeyOkay(pubEnc, exchangeSigningKeys)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const sig = decodeCrock(recoupConfirmation.exchange_sig);
|
||||||
|
const pub = decodeCrock(pubEnc);
|
||||||
|
|
||||||
|
if (recoupConfirmation.old_coin_pub) {
|
||||||
|
// We're dealing with a refresh recoup
|
||||||
|
const p = buildSigPS(
|
||||||
|
SignaturePurpose.EXCHANGE_CONFIRM_RECOUP_REFRESH,
|
||||||
|
).put(timestampToBuffer(recoupConfirmation.timestamp))
|
||||||
|
.put(amountToBuffer(Amounts.parseOrThrow(recoupConfirmation.amount)))
|
||||||
|
.put(decodeCrock(recoupCoinPub))
|
||||||
|
.put(decodeCrock(recoupConfirmation.old_coin_pub)).build();
|
||||||
|
return eddsaVerify(p, sig, pub)
|
||||||
|
} else if (recoupConfirmation.reserve_pub) {
|
||||||
|
const p = buildSigPS(
|
||||||
|
SignaturePurpose.EXCHANGE_CONFIRM_RECOUP_REFRESH,
|
||||||
|
).put(timestampToBuffer(recoupConfirmation.timestamp))
|
||||||
|
.put(amountToBuffer(Amounts.parseOrThrow(recoupConfirmation.amount)))
|
||||||
|
.put(decodeCrock(recoupCoinPub))
|
||||||
|
.put(decodeCrock(recoupConfirmation.reserve_pub)).build();
|
||||||
|
return eddsaVerify(p, sig, pub)
|
||||||
|
} else {
|
||||||
|
throw Error("invalid recoup confirmation");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
benchmark(repetitions: number): BenchmarkResult {
|
benchmark(repetitions: number): BenchmarkResult {
|
||||||
let time_hash = 0;
|
let time_hash = 0;
|
||||||
for (let i = 0; i < repetitions; i++) {
|
for (let i = 0; i < repetitions; i++) {
|
||||||
|
@ -211,12 +211,14 @@ async function updateExchangeWithKeys(
|
|||||||
if (r.details) {
|
if (r.details) {
|
||||||
// FIXME: We need to do some consistency checks!
|
// FIXME: We need to do some consistency checks!
|
||||||
}
|
}
|
||||||
|
// FIXME: validate signing keys and merge with old set
|
||||||
r.details = {
|
r.details = {
|
||||||
auditors: exchangeKeysJson.auditors,
|
auditors: exchangeKeysJson.auditors,
|
||||||
currency: currency,
|
currency: currency,
|
||||||
lastUpdateTime: lastUpdateTimestamp,
|
lastUpdateTime: lastUpdateTimestamp,
|
||||||
masterPublicKey: exchangeKeysJson.master_public_key,
|
masterPublicKey: exchangeKeysJson.master_public_key,
|
||||||
protocolVersion: protocolVersion,
|
protocolVersion: protocolVersion,
|
||||||
|
signingKeys: exchangeKeysJson.signkeys,
|
||||||
};
|
};
|
||||||
r.updateStatus = ExchangeUpdateStatus.FetchWire;
|
r.updateStatus = ExchangeUpdateStatus.FetchWire;
|
||||||
r.lastError = undefined;
|
r.lastError = undefined;
|
||||||
|
@ -142,7 +142,26 @@ async function recoupWithdrawCoin(
|
|||||||
throw Error(`Coin's reserve doesn't match reserve on recoup`);
|
throw Error(`Coin's reserve doesn't match reserve on recoup`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// FIXME: verify signature
|
const exchange = await ws.db.get(Stores.exchanges, coin.exchangeBaseUrl);
|
||||||
|
if (!exchange) {
|
||||||
|
// FIXME: report inconsistency?
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const exchangeDetails = exchange.details;
|
||||||
|
if (!exchangeDetails) {
|
||||||
|
// FIXME: report inconsistency?
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isValid = ws.cryptoApi.isValidRecoupConfirmation(
|
||||||
|
coin.coinPub,
|
||||||
|
recoupConfirmation,
|
||||||
|
exchangeDetails.signingKeys,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!isValid) {
|
||||||
|
throw Error("invalid recoup confirmation signature");
|
||||||
|
}
|
||||||
|
|
||||||
// FIXME: verify that our expectations about the amount match
|
// FIXME: verify that our expectations about the amount match
|
||||||
|
|
||||||
@ -207,6 +226,27 @@ async function recoupRefreshCoin(
|
|||||||
throw Error(`Coin's oldCoinPub doesn't match reserve on recoup`);
|
throw Error(`Coin's oldCoinPub doesn't match reserve on recoup`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const exchange = await ws.db.get(Stores.exchanges, coin.exchangeBaseUrl);
|
||||||
|
if (!exchange) {
|
||||||
|
// FIXME: report inconsistency?
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const exchangeDetails = exchange.details;
|
||||||
|
if (!exchangeDetails) {
|
||||||
|
// FIXME: report inconsistency?
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isValid = ws.cryptoApi.isValidRecoupConfirmation(
|
||||||
|
coin.coinPub,
|
||||||
|
recoupConfirmation,
|
||||||
|
exchangeDetails.signingKeys,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!isValid) {
|
||||||
|
throw Error("invalid recoup confirmation signature");
|
||||||
|
}
|
||||||
|
|
||||||
const refreshGroupId = await ws.db.runWithWriteTransaction(
|
const refreshGroupId = await ws.db.runWithWriteTransaction(
|
||||||
[Stores.coins, Stores.reserves],
|
[Stores.coins, Stores.reserves],
|
||||||
async tx => {
|
async tx => {
|
||||||
|
@ -30,6 +30,7 @@ import {
|
|||||||
MerchantRefundPermission,
|
MerchantRefundPermission,
|
||||||
PayReq,
|
PayReq,
|
||||||
TipResponse,
|
TipResponse,
|
||||||
|
ExchangeSignKeyJson,
|
||||||
} from "./talerTypes";
|
} from "./talerTypes";
|
||||||
|
|
||||||
import { Index, Store } from "../util/query";
|
import { Index, Store } from "../util/query";
|
||||||
@ -410,6 +411,7 @@ export interface ExchangeDetails {
|
|||||||
* Master public key of the exchange.
|
* Master public key of the exchange.
|
||||||
*/
|
*/
|
||||||
masterPublicKey: string;
|
masterPublicKey: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Auditors (partially) auditing the exchange.
|
* Auditors (partially) auditing the exchange.
|
||||||
*/
|
*/
|
||||||
@ -425,6 +427,12 @@ export interface ExchangeDetails {
|
|||||||
*/
|
*/
|
||||||
protocolVersion: string;
|
protocolVersion: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Signing keys we got from the exchange, can also contain
|
||||||
|
* older signing keys that are not returned by /keys anymore.
|
||||||
|
*/
|
||||||
|
signingKeys: ExchangeSignKeyJson[];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Timestamp for last update.
|
* Timestamp for last update.
|
||||||
*/
|
*/
|
||||||
|
@ -598,6 +598,17 @@ export class Recoup {
|
|||||||
h_denom_pub: string;
|
h_denom_pub: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Structure of one exchange signing key in the /keys response.
|
||||||
|
*/
|
||||||
|
export class ExchangeSignKeyJson {
|
||||||
|
stamp_start: Timestamp;
|
||||||
|
stamp_expire: Timestamp;
|
||||||
|
stamp_end: Timestamp;
|
||||||
|
key: EddsaPublicKeyString;
|
||||||
|
master_sig: EddsaSignatureString;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Structure that the exchange gives us in /keys.
|
* Structure that the exchange gives us in /keys.
|
||||||
*/
|
*/
|
||||||
@ -631,7 +642,7 @@ export class ExchangeKeysJson {
|
|||||||
* Short-lived signing keys used to sign online
|
* Short-lived signing keys used to sign online
|
||||||
* responses.
|
* responses.
|
||||||
*/
|
*/
|
||||||
signkeys: any;
|
signkeys: ExchangeSignKeyJson[];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Protocol version.
|
* Protocol version.
|
||||||
@ -881,6 +892,17 @@ export const codecForRecoup = () =>
|
|||||||
.build("Payback"),
|
.build("Payback"),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const codecForExchangeSigningKey = () =>
|
||||||
|
typecheckedCodec<ExchangeSignKeyJson>(
|
||||||
|
makeCodecForObject<ExchangeSignKeyJson>()
|
||||||
|
.property("key", codecForString)
|
||||||
|
.property("master_sig", codecForString)
|
||||||
|
.property("stamp_end", codecForTimestamp)
|
||||||
|
.property("stamp_start", codecForTimestamp)
|
||||||
|
.property("stamp_expire", codecForTimestamp)
|
||||||
|
.build("ExchangeSignKeyJson"),
|
||||||
|
);
|
||||||
|
|
||||||
export const codecForExchangeKeysJson = () =>
|
export const codecForExchangeKeysJson = () =>
|
||||||
typecheckedCodec<ExchangeKeysJson>(
|
typecheckedCodec<ExchangeKeysJson>(
|
||||||
makeCodecForObject<ExchangeKeysJson>()
|
makeCodecForObject<ExchangeKeysJson>()
|
||||||
@ -889,7 +911,7 @@ export const codecForExchangeKeysJson = () =>
|
|||||||
.property("auditors", makeCodecForList(codecForAuditor()))
|
.property("auditors", makeCodecForList(codecForAuditor()))
|
||||||
.property("list_issue_date", codecForTimestamp)
|
.property("list_issue_date", codecForTimestamp)
|
||||||
.property("recoup", makeCodecOptional(makeCodecForList(codecForRecoup())))
|
.property("recoup", makeCodecOptional(makeCodecForList(codecForRecoup())))
|
||||||
.property("signkeys", codecForAny)
|
.property("signkeys", makeCodecForList(codecForExchangeSigningKey()))
|
||||||
.property("version", codecForString)
|
.property("version", codecForString)
|
||||||
.build("KeysJson"),
|
.build("KeysJson"),
|
||||||
);
|
);
|
||||||
@ -981,7 +1003,6 @@ export const codecForRecoupConfirmation = () =>
|
|||||||
.build("RecoupConfirmation"),
|
.build("RecoupConfirmation"),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
export const codecForWithdrawResponse = () =>
|
export const codecForWithdrawResponse = () =>
|
||||||
typecheckedCodec<WithdrawResponse>(
|
typecheckedCodec<WithdrawResponse>(
|
||||||
makeCodecForObject<WithdrawResponse>()
|
makeCodecForObject<WithdrawResponse>()
|
||||||
|
@ -132,6 +132,16 @@ export function timestampDifference(t1: Timestamp, t2: Timestamp): Duration {
|
|||||||
return { d_ms: Math.abs(t1.t_ms - t2.t_ms) };
|
return { d_ms: Math.abs(t1.t_ms - t2.t_ms) };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function timestampIsBetween(t: Timestamp, start: Timestamp, end: Timestamp) {
|
||||||
|
if (timestampCmp(t, start) < 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (timestampCmp(t, end) > 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
export const codecForTimestamp: Codec<Timestamp> = {
|
export const codecForTimestamp: Codec<Timestamp> = {
|
||||||
decode(x: any, c?: Context): Timestamp {
|
decode(x: any, c?: Context): Timestamp {
|
||||||
const t_ms = x.t_ms;
|
const t_ms = x.t_ms;
|
||||||
|
Loading…
Reference in New Issue
Block a user