From f88e14f66d37c339816cb9ba73a84491e7133307 Mon Sep 17 00:00:00 2001 From: Florian Dold Date: Wed, 4 Aug 2021 17:14:52 +0200 Subject: [PATCH] towards exchange linting --- packages/taler-util/src/talerTypes.ts | 22 ++ packages/taler-wallet-cli/src/index.ts | 8 +- .../src/integrationtests/harness.ts | 43 --- packages/taler-wallet-cli/src/lint.ts | 265 +++++++++++++++++- 4 files changed, 288 insertions(+), 50 deletions(-) diff --git a/packages/taler-util/src/talerTypes.ts b/packages/taler-util/src/talerTypes.ts index 0df46c6db..56110ec1e 100644 --- a/packages/taler-util/src/talerTypes.ts +++ b/packages/taler-util/src/talerTypes.ts @@ -1429,3 +1429,25 @@ export const codecForTalerConfigResponse = (): Codec => .property("version", codecForString()) .property("currency", codecOptional(codecForString())) .build("TalerConfigResponse"); + +export interface FutureKeysResponse { + future_denoms: any[]; + + future_signkeys: any[]; + + master_pub: string; + + denom_secmod_public_key: string; + + // Public key of the signkey security module. + signkey_secmod_public_key: string; +} + +export const codecForKeysManagementResponse = (): Codec => + buildCodecForObject() + .property("master_pub", codecForString()) + .property("future_signkeys", codecForList(codecForAny())) + .property("future_denoms", codecForList(codecForAny())) + .property("denom_secmod_public_key", codecForAny()) + .property("signkey_secmod_public_key", codecForAny()) + .build("FutureKeysResponse"); diff --git a/packages/taler-wallet-cli/src/index.ts b/packages/taler-wallet-cli/src/index.ts index 26ee95661..f21e98f0a 100644 --- a/packages/taler-wallet-cli/src/index.ts +++ b/packages/taler-wallet-cli/src/index.ts @@ -55,7 +55,7 @@ import { WalletCoreApiClient, Wallet, } from "@gnu-taler/taler-wallet-core"; -import { lintDeployment } from "./lint.js"; +import { lintExchangeDeployment } from "./lint.js"; // This module also serves as the entry point for the crypto // thread worker, and thus must expose these two handlers. @@ -870,8 +870,10 @@ const deploymentCli = walletCli.subcommand("deploymentArgs", "deployment", { help: "Subcommands for handling GNU Taler deployments.", }); -deploymentCli.subcommand("lint", "lint").action(async (args) => { - lintDeployment(); +deploymentCli.subcommand("lintExchange", "lint-exchange", { + help: "Run checks on the exchange deployment." +}).action(async (args) => { + await lintExchangeDeployment(); }); deploymentCli diff --git a/packages/taler-wallet-cli/src/integrationtests/harness.ts b/packages/taler-wallet-cli/src/integrationtests/harness.ts index 18b06d1d1..285e9aa10 100644 --- a/packages/taler-wallet-cli/src/integrationtests/harness.ts +++ b/packages/taler-wallet-cli/src/integrationtests/harness.ts @@ -51,7 +51,6 @@ import { getRandomBytes, openPromise, OperationFailedError, - WalletApiOperation, WalletCoreApiClient, } from "@gnu-taler/taler-wallet-core"; import { @@ -65,49 +64,7 @@ import { Duration, parsePaytoUri, CoreApiResponse, - ApplyRefundRequest, - ApplyRefundResponse, - codecForApplyRefundResponse, - PreparePayRequest, - PreparePayResult, - codecForPreparePayResult, - CreateDepositGroupRequest, - CreateDepositGroupResponse, - AbortPayWithRefundRequest, - ConfirmPayRequest, - ConfirmPayResult, - codecForConfirmPayResult, - PrepareTipRequest, - PrepareTipResult, - codecForPrepareTipResult, - AcceptTipRequest, - CoinDumpJson, - codecForAny, - AddExchangeRequest, - ForceExchangeUpdateRequest, - ForceRefreshRequest, - ExchangesListRespose, - codecForExchangesListResponse, - BalancesResponse, - codecForBalancesResponse, - TransactionsResponse, - codecForTransactionsResponse, - TrackDepositGroupRequest, - TrackDepositGroupResponse, - IntegrationTestArgs, - TestPayArgs, - WithdrawTestBalanceRequest, - GetWithdrawalDetailsForUriRequest, - WithdrawUriInfoResponse, - codecForWithdrawUriInfoResponse, - BackupRecovery, - RecoveryLoadRequest, } from "@gnu-taler/taler-util"; -import { - AddBackupProviderRequest, - BackupInfo, -} from "@gnu-taler/taler-wallet-core/src/operations/backup"; -import { PendingOperationsResponse } from "@gnu-taler/taler-wallet-core/src/pending-types"; import { CoinConfig } from "./denomStructures.js"; const exec = util.promisify(require("child_process").exec); diff --git a/packages/taler-wallet-cli/src/lint.ts b/packages/taler-wallet-cli/src/lint.ts index ad00143b0..f7dfefe38 100644 --- a/packages/taler-wallet-cli/src/lint.ts +++ b/packages/taler-wallet-cli/src/lint.ts @@ -17,18 +17,94 @@ /** * Imports. */ -import { Configuration } from "@gnu-taler/taler-util"; +import { + buildCodecForObject, + Codec, + codecForAny, + codecForExchangeKeysJson, + codecForKeysManagementResponse, + codecForList, + codecForString, + Configuration, +} from "@gnu-taler/taler-util"; +import { + decodeCrock, + NodeHttpLib, + readSuccessResponseJsonOrThrow, +} from "@gnu-taler/taler-wallet-core"; +import { URL } from "url"; +import * as fs from "fs"; +import * as path from "path"; +import { ChildProcess, spawn } from "child_process"; + +interface BasicConf { + mainCurrency: string; +} + +interface PubkeyConf { + masterPublicKey: string; +} + +const httpLib = new NodeHttpLib(); + +interface ShellResult { + stdout: string; + stderr: string; + status: number; +} /** - * Do some basic checks in the configuration of a Taler deployment. + * Run a shell command, return stdout. */ -export function lintDeployment() { - const cfg = Configuration.load(); +export async function sh( + command: string, + env: { [index: string]: string | undefined } = process.env, +): Promise { + return new Promise((resolve, reject) => { + const stdoutChunks: Buffer[] = []; + const stderrChunks: Buffer[] = []; + const proc = spawn(command, { + stdio: ["inherit", "pipe", "pipe"], + shell: true, + env: env, + }); + proc.stdout.on("data", (x) => { + if (x instanceof Buffer) { + stdoutChunks.push(x); + } else { + throw Error("unexpected data chunk type"); + } + }); + proc.stderr.on("data", (x) => { + if (x instanceof Buffer) { + stderrChunks.push(x); + } else { + throw Error("unexpected data chunk type"); + } + }); + proc.on("exit", (code, signal) => { + console.log(`child process exited (${code} / ${signal})`); + const bOut = Buffer.concat(stdoutChunks).toString("utf-8"); + const bErr = Buffer.concat(stderrChunks).toString("utf-8"); + resolve({ + status: code ?? -1, + stderr: bErr, + stdout: bOut, + }); + }); + proc.on("error", () => { + reject(Error("Child process had error")); + }); + }); +} + +function checkBasicConf(cfg: Configuration): BasicConf { const currencyEntry = cfg.getString("taler", "currency"); let mainCurrency: string | undefined; if (!currencyEntry.value) { console.log("error: currency not defined in section TALER option CURRENCY"); + process.exit(1); } else { mainCurrency = currencyEntry.value.toUpperCase(); } @@ -38,4 +114,185 @@ export function lintDeployment() { "warning: section TALER option CURRENCY contains toy currency value KUDOS", ); } + + const roundUnit = cfg.getAmount("taler", "currency_round_unit"); + if (!roundUnit.isDefined) { + console.log( + "error: configuration incomplete, section TALER option CURRENCY_ROUND_UNIT missing", + ); + } + return { mainCurrency }; +} + +function checkCoinConfig(cfg: Configuration, basic: BasicConf): void { + const coinPrefix = "coin_"; + let numCoins = 0; + + for (const secName of cfg.getSectionNames()) { + if (!secName.startsWith(coinPrefix)) { + continue; + } + numCoins++; + + // FIXME: check that section is well-formed + } + + console.log( + "error: no coin denomination configured, please configure [coin_*] sections", + ); +} + +function checkWireConfig(cfg: Configuration): void { + const accountPrefix = "exchange-account-"; + const accountCredentialsPrefix = "exchange-accountcredentials-"; + + let accounts = new Set(); + let credentials = new Set(); + + for (const secName of cfg.getSectionNames()) { + if (secName.startsWith(accountPrefix)) { + accounts.add(secName.slice(accountPrefix.length)); + // FIXME: check settings + } + + if (secName.startsWith(accountCredentialsPrefix)) { + credentials.add(secName.slice(accountCredentialsPrefix.length)); + // FIXME: check settings + } + } + + for (const acc of accounts) { + if (!credentials.has(acc)) { + console.log( + `warning: no credentials configured for exchange-account-${acc}`, + ); + } + } + + // FIXME: now try to use taler-exchange-wire-gateway-client to connect! + // FIXME: run wirewatch in test mode here? + // FIXME: run transfer in test mode here? +} + +function checkAggregatorConfig(cfg: Configuration) { + // FIXME: run aggregator in test mode here +} + +function checkCloserConfig(cfg: Configuration) { + // FIXME: run closer in test mode here +} + +function checkMasterPublicKeyConfig(cfg: Configuration): PubkeyConf { + const pub = cfg.getString("exchange", "master_public_key"); + + if (!pub.isDefined) { + console.log("error: Master public key is not set."); + process.exit(1); + } + + const pubDecoded = decodeCrock(pub.required()); + + if (pubDecoded.length != 32) { + console.log("error: invalid master public key"); + process.exit(1); + } + + return { + masterPublicKey: pub.required(), + }; +} + +export async function checkExchangeHttpd( + cfg: Configuration, + pubConf: PubkeyConf, +): Promise { + const baseUrlEntry = cfg.getString("exchange", "base_url"); + + if (!baseUrlEntry.isDefined) { + console.log( + "error: configuration needs to specify section EXCHANGE option BASE_URL", + ); + process.exit(1); + } + + const baseUrl = baseUrlEntry.required(); + + if (!baseUrl.startsWith("http")) { + console.log( + "error: section EXCHANGE option BASE_URL needs to be an http or https URL", + ); + process.exit(1); + } + + if (!baseUrl.endsWith("/")) { + console.log( + "error: section EXCHANGE option BASE_URL needs to end with a slash", + ); + process.exit(1); + } + + if (!baseUrl.startsWith("https://")) { + console.log( + "warning: section EXCHANGE option BASE_URL: it is recommended to serve the exchange via HTTPS", + ); + process.exit(1); + } + + { + const mgmtUrl = new URL("management/keys", baseUrl); + const resp = await httpLib.get(mgmtUrl.href); + + const futureKeys = await readSuccessResponseJsonOrThrow( + resp, + codecForKeysManagementResponse(), + ); + + if (futureKeys.future_denoms.length > 0) { + console.log( + `warning: exchange has denomination keys that need to be signed by the offline signing procedure`, + ); + } + + if (futureKeys.future_signkeys.length > 0) { + console.log( + `warning: exchange has signing keys that need to be signed by the offline signing procedure`, + ); + } + } + + { + const keysUrl = new URL("keys", baseUrl); + const resp = await httpLib.get(keysUrl.href); + const keys = await readSuccessResponseJsonOrThrow( + resp, + codecForExchangeKeysJson(), + ); + } +} + +/** + * Do some basic checks in the configuration of a Taler deployment. + */ +export async function lintExchangeDeployment(): Promise { + if (process.getuid() != 1) { + console.log( + "warning: the exchange deployment linter is designed to be run as root", + ); + } + + const cfg = Configuration.load(); + + const basic = checkBasicConf(cfg); + + checkCoinConfig(cfg, basic); + + checkWireConfig(cfg); + + checkAggregatorConfig(cfg); + + checkCloserConfig(cfg); + + const pubConf = checkMasterPublicKeyConfig(cfg); + + await checkExchangeHttpd(cfg, pubConf); }