diff options
author | Florian Dold <florian.dold@gmail.com> | 2020-03-13 19:04:16 +0530 |
---|---|---|
committer | Florian Dold <florian.dold@gmail.com> | 2020-03-13 19:04:16 +0530 |
commit | 1744b1a80063397105081a4d5aeec76936781345 (patch) | |
tree | 53399350dba33fd6e7c916b3c177e36ff7e283f9 | |
parent | 51eef5419a37187f437115316a00ceec91e4addb (diff) |
signature verification for recoup
-rw-r--r-- | src/crypto/workers/cryptoApi.ts | 43 | ||||
-rw-r--r-- | src/crypto/workers/cryptoImplementation.ts | 74 | ||||
-rw-r--r-- | src/operations/exchanges.ts | 2 | ||||
-rw-r--r-- | src/operations/recoup.ts | 42 | ||||
-rw-r--r-- | src/types/dbTypes.ts | 8 | ||||
-rw-r--r-- | src/types/talerTypes.ts | 29 | ||||
-rw-r--r-- | src/util/time.ts | 10 |
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; |