towards exchange linting

This commit is contained in:
Florian Dold 2021-08-04 17:14:52 +02:00
parent 18c8cebbcd
commit f88e14f66d
No known key found for this signature in database
GPG Key ID: D2E4F00F29D02A4B
4 changed files with 288 additions and 50 deletions

View File

@ -1429,3 +1429,25 @@ export const codecForTalerConfigResponse = (): Codec<TalerConfigResponse> =>
.property("version", codecForString()) .property("version", codecForString())
.property("currency", codecOptional(codecForString())) .property("currency", codecOptional(codecForString()))
.build("TalerConfigResponse"); .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");

View File

@ -55,7 +55,7 @@ import {
WalletCoreApiClient, WalletCoreApiClient,
Wallet, Wallet,
} from "@gnu-taler/taler-wallet-core"; } 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 // This module also serves as the entry point for the crypto
// thread worker, and thus must expose these two handlers. // 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.", help: "Subcommands for handling GNU Taler deployments.",
}); });
deploymentCli.subcommand("lint", "lint").action(async (args) => { deploymentCli.subcommand("lintExchange", "lint-exchange", {
lintDeployment(); help: "Run checks on the exchange deployment."
}).action(async (args) => {
await lintExchangeDeployment();
}); });
deploymentCli deploymentCli

View File

@ -51,7 +51,6 @@ import {
getRandomBytes, getRandomBytes,
openPromise, openPromise,
OperationFailedError, OperationFailedError,
WalletApiOperation,
WalletCoreApiClient, WalletCoreApiClient,
} from "@gnu-taler/taler-wallet-core"; } from "@gnu-taler/taler-wallet-core";
import { import {
@ -65,49 +64,7 @@ import {
Duration, Duration,
parsePaytoUri, parsePaytoUri,
CoreApiResponse, 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"; } 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"; import { CoinConfig } from "./denomStructures.js";
const exec = util.promisify(require("child_process").exec); const exec = util.promisify(require("child_process").exec);

View File

@ -17,18 +17,94 @@
/** /**
* Imports. * 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() { export async function sh(
const cfg = Configuration.load(); 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"); const currencyEntry = cfg.getString("taler", "currency");
let mainCurrency: string | undefined; let mainCurrency: string | undefined;
if (!currencyEntry.value) { if (!currencyEntry.value) {
console.log("error: currency not defined in section TALER option CURRENCY"); console.log("error: currency not defined in section TALER option CURRENCY");
process.exit(1);
} else { } else {
mainCurrency = currencyEntry.value.toUpperCase(); mainCurrency = currencyEntry.value.toUpperCase();
} }
@ -38,4 +114,185 @@ export function lintDeployment() {
"warning: section TALER option CURRENCY contains toy currency value KUDOS", "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);
} }