aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorFlorian Dold <florian.dold@gmail.com>2020-03-13 19:04:16 +0530
committerFlorian Dold <florian.dold@gmail.com>2020-03-13 19:04:16 +0530
commit1744b1a80063397105081a4d5aeec76936781345 (patch)
tree53399350dba33fd6e7c916b3c177e36ff7e283f9
parent51eef5419a37187f437115316a00ceec91e4addb (diff)
signature verification for recoup
-rw-r--r--src/crypto/workers/cryptoApi.ts43
-rw-r--r--src/crypto/workers/cryptoImplementation.ts74
-rw-r--r--src/operations/exchanges.ts2
-rw-r--r--src/operations/recoup.ts42
-rw-r--r--src/types/dbTypes.ts8
-rw-r--r--src/types/talerTypes.ts29
-rw-r--r--src/util/time.ts10
7 files changed, 193 insertions, 15 deletions
diff --git a/src/crypto/workers/cryptoApi.ts b/src/crypto/workers/cryptoApi.ts
index 4adf2882e..31ab4dd7e 100644
--- a/src/crypto/workers/cryptoApi.ts
+++ b/src/crypto/workers/cryptoApi.ts
@@ -34,7 +34,13 @@ import {
import { CryptoWorker } from "./cryptoWorker";
-import { RecoupRequest, CoinDepositPermission } from "../../types/talerTypes";
+import {
+ RecoupRequest,
+ CoinDepositPermission,
+ RecoupConfirmation,
+ ExchangeSignKeyJson,
+ EddsaPublicKeyString,
+} from "../../types/talerTypes";
import {
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(
- depositInfo: DepositInfo
+ depositInfo: DepositInfo,
): Promise<CoinDepositPermission> {
return this.doRpc<CoinDepositPermission>(
"signDepositPermission",
3,
- depositInfo
+ depositInfo,
);
}
@@ -404,8 +427,18 @@ export class CryptoApi {
return this.doRpc<boolean>("rsaVerify", 4, hm, sig, pk);
}
- isValidWireAccount(paytoUri: string, sig: string, masterPub: string): Promise<boolean> {
- return this.doRpc<boolean>("isValidWireAccount", 4, paytoUri, sig, masterPub);
+ isValidWireAccount(
+ paytoUri: string,
+ sig: string,
+ masterPub: string,
+ ): Promise<boolean> {
+ return this.doRpc<boolean>(
+ "isValidWireAccount",
+ 4,
+ paytoUri,
+ sig,
+ masterPub,
+ );
}
createRecoupRequest(coin: CoinRecord): Promise<RecoupRequest> {
diff --git a/src/crypto/workers/cryptoImplementation.ts b/src/crypto/workers/cryptoImplementation.ts
index 5659fec21..4d03e70f5 100644
--- a/src/crypto/workers/cryptoImplementation.ts
+++ b/src/crypto/workers/cryptoImplementation.ts
@@ -1,6 +1,6 @@
/*
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
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.
*
* The functionality is parameterized over an Emscripten environment.
+ *
+ * @author Florian Dold <dold@taler.net>
*/
/**
@@ -34,7 +36,13 @@ import {
CoinSourceType,
} from "../../types/dbTypes";
-import { CoinDepositPermission, RecoupRequest } from "../../types/talerTypes";
+import {
+ CoinDepositPermission,
+ RecoupRequest,
+ RecoupConfirmation,
+ ExchangeSignKeyJson,
+ EddsaPublicKeyString,
+} from "../../types/talerTypes";
import {
BenchmarkResult,
PlanchetCreationResult,
@@ -63,7 +71,11 @@ import {
} from "../talerCrypto";
import { randomBytes } from "../primitives/nacl-fast";
import { kdf } from "../primitives/kdf";
-import { Timestamp, getTimestampNow } from "../../util/time";
+import {
+ Timestamp,
+ getTimestampNow,
+ timestampIsBetween,
+} from "../../util/time";
enum SignaturePurpose {
RESERVE_WITHDRAW = 1200,
@@ -76,6 +88,8 @@ enum SignaturePurpose {
MERCHANT_PAYMENT_OK = 1104,
WALLET_COIN_RECOUP = 1203,
WALLET_COIN_LINK = 1204,
+ EXCHANGE_CONFIRM_RECOUP = 1039,
+ EXCHANGE_CONFIRM_RECOUP_REFRESH = 1041,
}
function amountToBuffer(amount: AmountJson): Uint8Array {
@@ -131,6 +145,19 @@ function buildSigPS(purposeNum: number): SignaturePurposeBuilder {
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 {
static enableTracing: boolean = false;
@@ -216,7 +243,7 @@ export class CryptoImplementation {
coin_sig: encodeCrock(coinSig),
denom_pub_hash: coin.denomPubHash,
denom_sig: coin.denomSig,
- refreshed: (coin.coinSource.type === CoinSourceType.Refresh),
+ refreshed: coin.coinSource.type === CoinSourceType.Refresh,
};
return paybackRequest;
}
@@ -327,7 +354,6 @@ export class CryptoImplementation {
* and deposit permissions for each given coin.
*/
signDepositPermission(depositInfo: DepositInfo): CoinDepositPermission {
-
const d = buildSigPS(SignaturePurpose.WALLET_COIN_DEPOSIT)
.put(decodeCrock(depositInfo.contractTermsHash))
.put(decodeCrock(depositInfo.wireInfoHash))
@@ -492,6 +518,44 @@ export class CryptoImplementation {
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 {
let time_hash = 0;
for (let i = 0; i < repetitions; i++) {
diff --git a/src/operations/exchanges.ts b/src/operations/exchanges.ts
index 04238e61d..f920a5a59 100644
--- a/src/operations/exchanges.ts
+++ b/src/operations/exchanges.ts
@@ -211,12 +211,14 @@ async function updateExchangeWithKeys(
if (r.details) {
// FIXME: We need to do some consistency checks!
}
+ // FIXME: validate signing keys and merge with old set
r.details = {
auditors: exchangeKeysJson.auditors,
currency: currency,
lastUpdateTime: lastUpdateTimestamp,
masterPublicKey: exchangeKeysJson.master_public_key,
protocolVersion: protocolVersion,
+ signingKeys: exchangeKeysJson.signkeys,
};
r.updateStatus = ExchangeUpdateStatus.FetchWire;
r.lastError = undefined;
diff --git a/src/operations/recoup.ts b/src/operations/recoup.ts
index 29753ce28..163f77591 100644
--- a/src/operations/recoup.ts
+++ b/src/operations/recoup.ts
@@ -142,7 +142,26 @@ async function recoupWithdrawCoin(
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
@@ -207,6 +226,27 @@ async function recoupRefreshCoin(
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(
[Stores.coins, Stores.reserves],
async tx => {
diff --git a/src/types/dbTypes.ts b/src/types/dbTypes.ts
index 36b45f5ac..f28426ac9 100644
--- a/src/types/dbTypes.ts
+++ b/src/types/dbTypes.ts
@@ -30,6 +30,7 @@ import {
MerchantRefundPermission,
PayReq,
TipResponse,
+ ExchangeSignKeyJson,
} from "./talerTypes";
import { Index, Store } from "../util/query";
@@ -410,6 +411,7 @@ export interface ExchangeDetails {
* Master public key of the exchange.
*/
masterPublicKey: string;
+
/**
* Auditors (partially) auditing the exchange.
*/
@@ -426,6 +428,12 @@ export interface ExchangeDetails {
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.
*/
lastUpdateTime: Timestamp;
diff --git a/src/types/talerTypes.ts b/src/types/talerTypes.ts
index 2ecb82340..569b93120 100644
--- a/src/types/talerTypes.ts
+++ b/src/types/talerTypes.ts
@@ -599,6 +599,17 @@ export class Recoup {
}
/**
+ * 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.
*/
export class ExchangeKeysJson {
@@ -631,7 +642,7 @@ export class ExchangeKeysJson {
* Short-lived signing keys used to sign online
* responses.
*/
- signkeys: any;
+ signkeys: ExchangeSignKeyJson[];
/**
* Protocol version.
@@ -881,6 +892,17 @@ export const codecForRecoup = () =>
.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 = () =>
typecheckedCodec<ExchangeKeysJson>(
makeCodecForObject<ExchangeKeysJson>()
@@ -889,7 +911,7 @@ export const codecForExchangeKeysJson = () =>
.property("auditors", makeCodecForList(codecForAuditor()))
.property("list_issue_date", codecForTimestamp)
.property("recoup", makeCodecOptional(makeCodecForList(codecForRecoup())))
- .property("signkeys", codecForAny)
+ .property("signkeys", makeCodecForList(codecForExchangeSigningKey()))
.property("version", codecForString)
.build("KeysJson"),
);
@@ -981,10 +1003,9 @@ export const codecForRecoupConfirmation = () =>
.build("RecoupConfirmation"),
);
-
export const codecForWithdrawResponse = () =>
typecheckedCodec<WithdrawResponse>(
makeCodecForObject<WithdrawResponse>()
.property("ev_sig", codecForString)
.build("WithdrawResponse"),
- ); \ No newline at end of file
+ );
diff --git a/src/util/time.ts b/src/util/time.ts
index 54d22bf81..88297f9a9 100644
--- a/src/util/time.ts
+++ b/src/util/time.ts
@@ -132,6 +132,16 @@ export function timestampDifference(t1: Timestamp, t2: Timestamp): Duration {
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> = {
decode(x: any, c?: Context): Timestamp {
const t_ms = x.t_ms;