aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorFlorian Dold <florian@dold.me>2021-08-04 17:14:52 +0200
committerFlorian Dold <florian@dold.me>2021-08-04 17:14:52 +0200
commitf88e14f66d37c339816cb9ba73a84491e7133307 (patch)
treea6709627512ff9f7e6fd4a14d7a99669c9a95949
parent18c8cebbcd85e7345607572be4c66dff940ad776 (diff)
towards exchange linting
-rw-r--r--packages/taler-util/src/talerTypes.ts22
-rw-r--r--packages/taler-wallet-cli/src/index.ts8
-rw-r--r--packages/taler-wallet-cli/src/integrationtests/harness.ts43
-rw-r--r--packages/taler-wallet-cli/src/lint.ts265
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<TalerConfigResponse> =>
.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<FutureKeysResponse> =>
+ buildCodecForObject<FutureKeysResponse>()
+ .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<ShellResult> {
+ 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<string>();
+ let credentials = new Set<string>();
+
+ 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<void> {
+ 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<void> {
+ 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);
}