wallet: crypto worker fixes, better taler-crypto-worker integration

This commit is contained in:
Florian Dold 2022-03-24 01:10:34 +01:00
parent cc18751e72
commit 9d38cb56a6
No known key found for this signature in database
GPG Key ID: D2E4F00F29D02A4B
7 changed files with 234 additions and 81 deletions

View File

@ -692,7 +692,7 @@ export interface FreshCoin {
bks: Uint8Array; bks: Uint8Array;
} }
function bufferForUint32(n: number): Uint8Array { export function bufferForUint32(n: number): Uint8Array {
const arrBuf = new ArrayBuffer(4); const arrBuf = new ArrayBuffer(4);
const buf = new Uint8Array(arrBuf); const buf = new Uint8Array(arrBuf);
const dv = new DataView(arrBuf); const dv = new DataView(arrBuf);
@ -700,37 +700,6 @@ function bufferForUint32(n: number): Uint8Array {
return buf; return buf;
} }
export function setupRefreshPlanchet(
transferSecret: Uint8Array,
coinNumber: number,
): FreshCoin {
// See TALER_transfer_secret_to_planchet_secret in C impl
const planchetMasterSecret = kdfKw({
ikm: transferSecret,
outputLength: 32,
salt: bufferForUint32(coinNumber),
info: stringToBytes("taler-coin-derivation"),
});
const coinPriv = kdfKw({
ikm: planchetMasterSecret,
outputLength: 32,
salt: stringToBytes("coin"),
});
const bks = kdfKw({
ikm: planchetMasterSecret,
outputLength: 32,
salt: stringToBytes("bks"),
});
return {
bks,
coinPriv,
coinPub: eddsaGetPublic(coinPriv),
};
}
export function setupWithdrawPlanchet( export function setupWithdrawPlanchet(
secretSeed: Uint8Array, secretSeed: Uint8Array,
coinNumber: number, coinNumber: number,
@ -786,10 +755,10 @@ export function setupRefreshTransferPub(
} }
/** /**
* *
* @param paytoUri * @param paytoUri
* @param salt 16-byte salt * @param salt 16-byte salt
* @returns * @returns
*/ */
export function hashWire(paytoUri: string, salt: string): string { export function hashWire(paytoUri: string, salt: string): string {
const r = kdf( const r = kdf(

View File

@ -64,7 +64,10 @@ import { runBench1 } from "./bench1.js";
import { runEnv1 } from "./env1.js"; import { runEnv1 } from "./env1.js";
import { GlobalTestState, runTestWithState } from "./harness/harness.js"; import { GlobalTestState, runTestWithState } from "./harness/harness.js";
import { runBench2 } from "./bench2.js"; import { runBench2 } from "./bench2.js";
import { TalerCryptoInterface, TalerCryptoInterfaceR } from "@gnu-taler/taler-wallet-core/src/crypto/cryptoImplementation"; import {
TalerCryptoInterface,
TalerCryptoInterfaceR,
} from "@gnu-taler/taler-wallet-core/src/crypto/cryptoImplementation";
// 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.
@ -75,6 +78,12 @@ export {
const logger = new Logger("taler-wallet-cli.ts"); const logger = new Logger("taler-wallet-cli.ts");
process.on("unhandledRejection", (error: any) => {
logger.error("unhandledRejection", error.message);
logger.error("stack", error.stack);
process.exit(2);
});
const defaultWalletDbPath = os.homedir + "/" + ".talerwalletdb.json"; const defaultWalletDbPath = os.homedir + "/" + ".talerwalletdb.json";
function assertUnreachable(x: never): never { function assertUnreachable(x: never): never {
@ -218,6 +227,7 @@ async function withWallet<T>(
} finally { } finally {
logger.info("operation with wallet finished, stopping"); logger.info("operation with wallet finished, stopping");
wallet.stop(); wallet.stop();
logger.info("stopped wallet");
} }
} }
@ -250,13 +260,18 @@ walletCli
console.error("Invalid JSON"); console.error("Invalid JSON");
process.exit(1); process.exit(1);
} }
const resp = await wallet.ws.handleCoreApiRequest( try {
args.api.operation, const resp = await wallet.ws.handleCoreApiRequest(
"reqid-1", args.api.operation,
requestJson, "reqid-1",
); requestJson,
console.log(JSON.stringify(resp, undefined, 2)); );
console.log(JSON.stringify(resp, undefined, 2));
} catch (e) {
logger.error(`Got exception while handling API request ${e}`);
}
}); });
logger.info("finished handling API request");
}); });
walletCli walletCli

View File

@ -42,7 +42,6 @@ import {
eddsaVerify, eddsaVerify,
encodeCrock, encodeCrock,
ExchangeProtocolVersion, ExchangeProtocolVersion,
FreshCoin,
hash, hash,
hashCoinEv, hashCoinEv,
hashCoinEvInner, hashCoinEvInner,
@ -53,14 +52,12 @@ import {
MakeSyncSignatureRequest, MakeSyncSignatureRequest,
PlanchetCreationRequest, PlanchetCreationRequest,
WithdrawalPlanchet, WithdrawalPlanchet,
randomBytes,
RecoupRefreshRequest, RecoupRefreshRequest,
RecoupRequest, RecoupRequest,
RefreshPlanchetInfo, RefreshPlanchetInfo,
rsaBlind, rsaBlind,
rsaUnblind, rsaUnblind,
rsaVerify, rsaVerify,
setupRefreshPlanchet,
setupRefreshTransferPub, setupRefreshTransferPub,
setupTipPlanchet, setupTipPlanchet,
setupWithdrawPlanchet, setupWithdrawPlanchet,
@ -70,6 +67,8 @@ import {
UnblindedSignature, UnblindedSignature,
PlanchetUnblindInfo, PlanchetUnblindInfo,
TalerProtocolTimestamp, TalerProtocolTimestamp,
kdfKw,
bufferForUint32,
} from "@gnu-taler/taler-util"; } from "@gnu-taler/taler-util";
import bigint from "big-integer"; import bigint from "big-integer";
import { DenominationRecord, WireFee } from "../db.js"; import { DenominationRecord, WireFee } from "../db.js";
@ -141,6 +140,8 @@ export interface TalerCryptoInterface {
rsaVerify(req: RsaVerificationRequest): Promise<ValidationResult>; rsaVerify(req: RsaVerificationRequest): Promise<ValidationResult>;
rsaBlind(req: RsaBlindRequest): Promise<RsaBlindResponse>;
signDepositPermission( signDepositPermission(
depositInfo: DepositInfo, depositInfo: DepositInfo,
): Promise<CoinDepositPermission>; ): Promise<CoinDepositPermission>;
@ -154,6 +155,14 @@ export interface TalerCryptoInterface {
signCoinLink(req: SignCoinLinkRequest): Promise<EddsaSigningResult>; signCoinLink(req: SignCoinLinkRequest): Promise<EddsaSigningResult>;
makeSyncSignature(req: MakeSyncSignatureRequest): Promise<EddsaSigningResult>; makeSyncSignature(req: MakeSyncSignatureRequest): Promise<EddsaSigningResult>;
setupRefreshPlanchet(
req: SetupRefreshPlanchetRequest,
): Promise<FreshCoinEncoded>;
keyExchangeEcdheEddsa(
req: KeyExchangeEcdheEddsaRequest,
): Promise<KeyExchangeResult>;
} }
/** /**
@ -257,6 +266,19 @@ export const nullCrypto: TalerCryptoInterface = {
): Promise<EddsaSigningResult> { ): Promise<EddsaSigningResult> {
throw new Error("Function not implemented."); throw new Error("Function not implemented.");
}, },
setupRefreshPlanchet: function (
req: SetupRefreshPlanchetRequest,
): Promise<FreshCoinEncoded> {
throw new Error("Function not implemented.");
},
rsaBlind: function (req: RsaBlindRequest): Promise<RsaBlindResponse> {
throw new Error("Function not implemented.");
},
keyExchangeEcdheEddsa: function (
req: KeyExchangeEcdheEddsaRequest,
): Promise<KeyExchangeResult> {
throw new Error("Function not implemented.");
},
}; };
export type WithArg<X> = X extends (req: infer T) => infer R export type WithArg<X> = X extends (req: infer T) => infer R
@ -275,12 +297,23 @@ export interface SignCoinLinkRequest {
coinEv: CoinEnvelope; coinEv: CoinEnvelope;
} }
export interface SetupRefreshPlanchetRequest {
transferSecret: string;
coinNumber: number;
}
export interface RsaVerificationRequest { export interface RsaVerificationRequest {
hm: string; hm: string;
sig: string; sig: string;
pk: string; pk: string;
} }
export interface RsaBlindRequest {
hm: string;
bks: string;
pub: string;
}
export interface EddsaSigningResult { export interface EddsaSigningResult {
sig: string; sig: string;
} }
@ -341,16 +374,35 @@ export interface UnblindDenominationSignatureRequest {
evSig: BlindedDenominationSignature; evSig: BlindedDenominationSignature;
} }
export interface FreshCoinEncoded {
coinPub: string;
coinPriv: string;
bks: string;
}
export interface RsaUnblindRequest { export interface RsaUnblindRequest {
blindedSig: string; blindedSig: string;
bk: string; bk: string;
pk: string; pk: string;
} }
export interface RsaBlindResponse {
blinded: string;
}
export interface RsaUnblindResponse { export interface RsaUnblindResponse {
sig: string; sig: string;
} }
export interface KeyExchangeEcdheEddsaRequest {
ecdhePriv: string;
eddsaPub: string;
}
export interface KeyExchangeResult {
h: string;
}
export const nativeCryptoR: TalerCryptoInterfaceR = { export const nativeCryptoR: TalerCryptoInterfaceR = {
async eddsaSign( async eddsaSign(
tci: TalerCryptoInterfaceR, tci: TalerCryptoInterfaceR,
@ -361,6 +413,53 @@ export const nativeCryptoR: TalerCryptoInterfaceR = {
}; };
}, },
async rsaBlind(
tci: TalerCryptoInterfaceR,
req: RsaBlindRequest,
): Promise<RsaBlindResponse> {
const res = rsaBlind(
decodeCrock(req.hm),
decodeCrock(req.bks),
decodeCrock(req.pub),
);
return {
blinded: encodeCrock(res),
};
},
async setupRefreshPlanchet(
tci: TalerCryptoInterfaceR,
req: SetupRefreshPlanchetRequest,
): Promise<FreshCoinEncoded> {
const transferSecret = decodeCrock(req.transferSecret);
const coinNumber = req.coinNumber;
// See TALER_transfer_secret_to_planchet_secret in C impl
const planchetMasterSecret = kdfKw({
ikm: transferSecret,
outputLength: 32,
salt: bufferForUint32(coinNumber),
info: stringToBytes("taler-coin-derivation"),
});
const coinPriv = kdfKw({
ikm: planchetMasterSecret,
outputLength: 32,
salt: stringToBytes("coin"),
});
const bks = kdfKw({
ikm: planchetMasterSecret,
outputLength: 32,
salt: stringToBytes("bks"),
});
return {
bks: encodeCrock(bks),
coinPriv: encodeCrock(coinPriv),
coinPub: encodeCrock(eddsaGetPublic(coinPriv)),
};
},
async createPlanchet( async createPlanchet(
tci: TalerCryptoInterfaceR, tci: TalerCryptoInterfaceR,
req: PlanchetCreationRequest, req: PlanchetCreationRequest,
@ -374,10 +473,14 @@ export const nativeCryptoR: TalerCryptoInterfaceR = {
req.coinIndex, req.coinIndex,
); );
const coinPubHash = hash(derivedPlanchet.coinPub); const coinPubHash = hash(derivedPlanchet.coinPub);
const ev = rsaBlind(coinPubHash, derivedPlanchet.bks, denomPubRsa); const blindResp = await tci.rsaBlind(tci, {
bks: encodeCrock(derivedPlanchet.bks),
hm: encodeCrock(coinPubHash),
pub: denomPub.rsa_public_key,
});
const coinEv: CoinEnvelope = { const coinEv: CoinEnvelope = {
cipher: DenomKeyType.Rsa, cipher: DenomKeyType.Rsa,
rsa_blinded_planchet: encodeCrock(ev), rsa_blinded_planchet: blindResp.blinded,
}; };
const amountWithFee = Amounts.add(req.value, req.feeWithdraw).amount; const amountWithFee = Amounts.add(req.value, req.feeWithdraw).amount;
const denomPubHash = hashDenomPub(req.denomPub); const denomPubHash = hashDenomPub(req.denomPub);
@ -423,10 +526,14 @@ export const nativeCryptoR: TalerCryptoInterfaceR = {
const fc = setupTipPlanchet(decodeCrock(req.secretSeed), req.planchetIndex); const fc = setupTipPlanchet(decodeCrock(req.secretSeed), req.planchetIndex);
const denomPub = decodeCrock(req.denomPub.rsa_public_key); const denomPub = decodeCrock(req.denomPub.rsa_public_key);
const coinPubHash = hash(fc.coinPub); const coinPubHash = hash(fc.coinPub);
const ev = rsaBlind(coinPubHash, fc.bks, denomPub); const blindResp = await tci.rsaBlind(tci, {
bks: encodeCrock(fc.bks),
hm: encodeCrock(coinPubHash),
pub: encodeCrock(denomPub),
});
const coinEv = { const coinEv = {
cipher: DenomKeyType.Rsa, cipher: DenomKeyType.Rsa,
rsa_blinded_planchet: encodeCrock(ev), rsa_blinded_planchet: blindResp.blinded,
}; };
const tipPlanchet: DerivedTipPlanchet = { const tipPlanchet: DerivedTipPlanchet = {
blindingKey: encodeCrock(fc.bks), blindingKey: encodeCrock(fc.bks),
@ -798,32 +905,32 @@ export const nativeCryptoR: TalerCryptoInterfaceR = {
const denomSel = newCoinDenoms[j]; const denomSel = newCoinDenoms[j];
for (let k = 0; k < denomSel.count; k++) { for (let k = 0; k < denomSel.count; k++) {
const coinIndex = planchets.length; const coinIndex = planchets.length;
const transferPriv = decodeCrock(transferPrivs[i]); const transferSecretRes = await tci.keyExchangeEcdheEddsa(tci, {
const oldCoinPub = decodeCrock(meltCoinPub); ecdhePriv: transferPrivs[i],
const transferSecret = keyExchangeEcdheEddsa( eddsaPub: meltCoinPub,
transferPriv, });
oldCoinPub,
);
let coinPub: Uint8Array; let coinPub: Uint8Array;
let coinPriv: Uint8Array; let coinPriv: Uint8Array;
let blindingFactor: Uint8Array; let blindingFactor: Uint8Array;
// FIXME: make setupRefreshPlanchet a crypto api fn let fresh: FreshCoinEncoded = await tci.setupRefreshPlanchet(tci, {
let fresh: FreshCoin = setupRefreshPlanchet( coinNumber: coinIndex,
transferSecret, transferSecret: transferSecretRes.h,
coinIndex, });
); coinPriv = decodeCrock(fresh.coinPriv);
coinPriv = fresh.coinPriv; coinPub = decodeCrock(fresh.coinPub);
coinPub = fresh.coinPub; blindingFactor = decodeCrock(fresh.bks);
blindingFactor = fresh.bks;
const coinPubHash = hash(coinPub); const coinPubHash = hash(coinPub);
if (denomSel.denomPub.cipher !== DenomKeyType.Rsa) { if (denomSel.denomPub.cipher !== DenomKeyType.Rsa) {
throw Error("unsupported cipher, can't create refresh session"); throw Error("unsupported cipher, can't create refresh session");
} }
const rsaDenomPub = decodeCrock(denomSel.denomPub.rsa_public_key); const blindResult = await tci.rsaBlind(tci, {
const ev = rsaBlind(coinPubHash, blindingFactor, rsaDenomPub); bks: encodeCrock(blindingFactor),
hm: encodeCrock(coinPubHash),
pub: denomSel.denomPub.rsa_public_key,
});
const coinEv: CoinEnvelope = { const coinEv: CoinEnvelope = {
cipher: DenomKeyType.Rsa, cipher: DenomKeyType.Rsa,
rsa_blinded_planchet: encodeCrock(ev), rsa_blinded_planchet: blindResult.blinded,
}; };
const coinEvHash = hashCoinEv( const coinEvHash = hashCoinEv(
coinEv, coinEv,
@ -921,6 +1028,19 @@ export const nativeCryptoR: TalerCryptoInterfaceR = {
const uploadSig = eddsaSign(sigBlob, decodeCrock(req.accountPriv)); const uploadSig = eddsaSign(sigBlob, decodeCrock(req.accountPriv));
return { sig: encodeCrock(uploadSig) }; return { sig: encodeCrock(uploadSig) };
}, },
async keyExchangeEcdheEddsa(
tci: TalerCryptoInterfaceR,
req: KeyExchangeEcdheEddsaRequest,
): Promise<KeyExchangeResult> {
return {
h: encodeCrock(
keyExchangeEcdheEddsa(
decodeCrock(req.ecdhePriv),
decodeCrock(req.eddsaPub),
),
),
};
},
}; };
function amountToBuffer(amount: AmountJson): Uint8Array { function amountToBuffer(amount: AmountJson): Uint8Array {

View File

@ -122,7 +122,7 @@ export class CryptoDispatcher {
worker.idleTimeoutHandle = null; worker.idleTimeoutHandle = null;
} }
if (worker.currentWorkItem) { if (worker.currentWorkItem) {
worker.currentWorkItem.reject(Error("explicitly terminated")); worker.currentWorkItem.reject(new CryptoApiStoppedError());
worker.currentWorkItem = null; worker.currentWorkItem = null;
} }
if (worker.w) { if (worker.w) {
@ -143,7 +143,7 @@ export class CryptoDispatcher {
*/ */
wake(ws: WorkerState, work: WorkItem): void { wake(ws: WorkerState, work: WorkItem): void {
if (this.stopped) { if (this.stopped) {
throw new CryptoApiStoppedError(); return;
} }
if (ws.currentWorkItem !== null) { if (ws.currentWorkItem !== null) {
throw Error("assertion failed"); throw Error("assertion failed");
@ -331,8 +331,8 @@ export class CryptoDispatcher {
} }
timeout.clear(); timeout.clear();
resolve(x); resolve(x);
}); }).catch((x) => {
p.catch((x) => { logger.info(`crypto RPC call ${operation} threw`);
if (timedOut) { if (timedOut) {
return; return;
} }

View File

@ -47,14 +47,11 @@ export class SynchronousCryptoWorker {
this.cryptoImplR = { ...nativeCryptoR }; this.cryptoImplR = { ...nativeCryptoR };
if ( if (process.env["TALER_WALLET_PRIMITIVE_WORKER"]) {
process.env["TALER_WALLET_RPC_CRYPRO"] || logger.info("using RPC for some crypto operations");
// Old name
process.env["TALER_WALLET_PRIMITIVE_WORKER"]
) {
const rpc = (this.rpcClient = new CryptoRpcClient()); const rpc = (this.rpcClient = new CryptoRpcClient());
this.cryptoImplR.eddsaSign = async (_, req) => { this.cryptoImplR.eddsaSign = async (_, req) => {
logger.trace("making RPC request"); logger.info("calling RPC impl of eddsaSign");
return await rpc.queueRequest({ return await rpc.queueRequest({
op: "eddsa_sign", op: "eddsa_sign",
args: { args: {
@ -63,6 +60,46 @@ export class SynchronousCryptoWorker {
}, },
}); });
}; };
this.cryptoImplR.setupRefreshPlanchet = async (_, req) => {
const res = await rpc.queueRequest({
op: "setup_refresh_planchet",
args: {
coin_index: req.coinNumber,
transfer_secret: req.transferSecret,
},
});
return {
bks: res.blinding_key,
coinPriv: res.coin_priv,
coinPub: res.coin_pub,
};
};
this.cryptoImplR.rsaBlind = async (_, req) => {
const res = await rpc.queueRequest({
op: "rsa_blind",
args: {
bks: req.bks,
hm: req.hm,
pub: req.pub,
},
});
return {
blinded: res.blinded,
};
};
this.cryptoImplR.keyExchangeEcdheEddsa = async (_, req) => {
const res = await rpc.queueRequest({
op: "kx_ecdhe_eddsa",
args: {
ecdhe_priv: req.ecdhePriv,
eddsa_pub: req.eddsaPub,
},
});
return {
h: res.h,
};
};
} }
} }
@ -101,8 +138,8 @@ export class SynchronousCryptoWorker {
let result: any; let result: any;
try { try {
result = await (impl as any)[operation](impl, req); result = await (impl as any)[operation](impl, req);
} catch (e) { } catch (e: any) {
logger.error("error during operation", e); logger.error(`error during operation '${operation}': ${e}`);
return; return;
} }

View File

@ -797,11 +797,22 @@ async function processRefreshGroupImpl(
return; return;
} }
// Process refresh sessions of the group in parallel. // Process refresh sessions of the group in parallel.
logger.trace("processing refresh sessions for old coins");
const ps = refreshGroup.oldCoinPubs.map((x, i) => const ps = refreshGroup.oldCoinPubs.map((x, i) =>
processRefreshSession(ws, refreshGroupId, i), processRefreshSession(ws, refreshGroupId, i).catch((x) => {
logger.warn("process refresh session got exception");
logger.warn(`exc ${x}`);
logger.warn(`exc stack ${x.stack}`);
}),
); );
await Promise.all(ps); try {
logger.trace("refresh finished"); logger.trace("waiting for refreshes");
await Promise.all(ps);
logger.trace("refresh finished");
} catch (e) {
logger.warn("process refresh sessions got exception");
logger.warn(`exception: ${e}`);
}
} }
async function processRefreshSession( async function processRefreshSession(

View File

@ -1077,6 +1077,7 @@ export async function handleCoreApiRequest(
}; };
} catch (e: any) { } catch (e: any) {
const err = getErrorDetailFromException(e); const err = getErrorDetailFromException(e);
logger.info(`finished wallet core request with error: ${j2s(err)}`);
return { return {
type: "error", type: "error",
operation, operation,