signature verification for recoup

This commit is contained in:
Florian Dold 2020-03-13 19:04:16 +05:30
parent 51eef5419a
commit 1744b1a800
No known key found for this signature in database
GPG Key ID: D2E4F00F29D02A4B
7 changed files with 193 additions and 15 deletions

View File

@ -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> {

View File

@ -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++) {

View File

@ -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;

View File

@ -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 => {

View File

@ -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.
*/ */

View File

@ -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>()

View File

@ -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;