commit b58dc3d60f5f19cbcd487847dedc4c720fea7e3e
parent 09d66257f8dac79ee1e05fa85ce1c11585c55aee
Author: Florian Dold <florian@dold.me>
Date: Tue, 22 Apr 2025 23:14:36 +0200
wallet-core: towards refreshV2 crypto implementation
Diffstat:
6 files changed, 202 insertions(+), 48 deletions(-)
diff --git a/packages/taler-harness/src/harness/harness.ts b/packages/taler-harness/src/harness/harness.ts
@@ -425,16 +425,20 @@ export class GlobalTestState {
logger.warn(`process ${logName} exited ${j2s({ code, signal })}`);
}
});
+ const stdoutLogFileName = this.testDir + `/${logName}-stdout.log`;
+ const stdoutLog = fs.createWriteStream(stdoutLogFileName, {
+ flags: "a",
+ autoClose: true,
+ });
const stderrLogFileName = this.testDir + `/${logName}-stderr.log`;
const stderrLog = fs.createWriteStream(stderrLogFileName, {
flags: "a",
+ autoClose: true,
});
proc.stderr.pipe(stderrLog);
- const stdoutLogFileName = this.testDir + `/${logName}-stdout.log`;
- const stdoutLog = fs.createWriteStream(stdoutLogFileName, {
- flags: "a",
+ proc.stdout.pipe(stdoutLog).on("error", (err) => {
+ console.error(err);
});
- proc.stdout.pipe(stdoutLog);
const procWrap = new ProcessWrapper(proc);
this.procs.push(procWrap);
return procWrap;
diff --git a/packages/taler-harness/src/integrationtests/test-kyc-two-forms.ts b/packages/taler-harness/src/integrationtests/test-kyc-two-forms.ts
@@ -19,7 +19,7 @@
*/
import {
AmountString,
- amountToBuffer,
+ bufferFromAmount,
buildSigPS,
codecForAccountKycStatus,
codecForKycProcessClientInformation,
@@ -158,7 +158,7 @@ export async function runKycTwoFormsTest(t: GlobalTestState) {
const balance: AmountString = "TESTKUDOS:20";
const sigBlob = buildSigPS(TalerSignaturePurpose.WALLET_ACCOUNT_SETUP)
- .put(amountToBuffer(balance))
+ .put(bufferFromAmount(balance))
.build();
const body: WalletKycRequest = {
balance,
diff --git a/packages/taler-util/src/taler-crypto.ts b/packages/taler-util/src/taler-crypto.ts
@@ -37,10 +37,7 @@ import {
DenomKeyType,
DenominationPubKey,
} from "./types-taler-exchange.js";
-import {
- TokenEnvelope,
- TokenIssuePublicKey,
-} from "./types-taler-merchant.js";
+import { TokenEnvelope, TokenIssuePublicKey } from "./types-taler-merchant.js";
import { PayWalletData } from "./types-taler-wallet.js";
const isEddsaPubP: unique symbol = Symbol("isEddsaPubP");
@@ -972,9 +969,7 @@ export function hashTokenEvInner(
}
}
-export function hashPayWalletData(
- walletData: PayWalletData
-): Uint8Array {
+export function hashPayWalletData(walletData: PayWalletData): Uint8Array {
const canon = canonicalJson(walletData) + "\0";
const bytes = stringToBytes(canon);
return hash(bytes);
@@ -1690,7 +1685,7 @@ export async function decryptContractForDeposit(
};
}
-export function amountToBuffer(amount: AmountLike): Uint8Array {
+export function bufferFromAmount(amount: AmountLike): Uint8Array {
const amountJ = Amounts.jsonifyAmount(amount);
const buffer = new ArrayBuffer(8 + 4 + 12);
const dvbuf = new DataView(buffer);
diff --git a/packages/taler-util/src/taler-signatures.ts b/packages/taler-util/src/taler-signatures.ts
@@ -16,7 +16,7 @@
import { AmountLike, canonicalJson, TalerProtocolTimestamp } from "./index.js";
import {
- amountToBuffer,
+ bufferFromAmount,
bufferForUint64,
buildSigPS,
decodeCrock,
@@ -86,7 +86,7 @@ export function signWalletAccountSetup(
balance: AmountLike,
): EddsaSigP {
const sigBlob = buildSigPS(TalerSignaturePurpose.WALLET_ACCOUNT_SETUP)
- .put(amountToBuffer(balance))
+ .put(bufferFromAmount(balance))
.build();
return eddsaSign(sigBlob, key);
diff --git a/packages/taler-wallet-core/src/crypto/cryptoImplementation.ts b/packages/taler-wallet-core/src/crypto/cryptoImplementation.ts
@@ -30,10 +30,10 @@ import {
AmountJson,
Amounts,
AmountString,
- amountToBuffer,
BlindedDenominationSignature,
bufferForUint32,
bufferForUint64,
+ bufferFromAmount,
buildSigPS,
canonicalJson,
CoinDepositPermission,
@@ -908,7 +908,7 @@ export const nativeCryptoR: TalerCryptoInterfaceR = {
const withdrawRequest = buildSigPS(
TalerSignaturePurpose.WALLET_RESERVE_WITHDRAW,
)
- .put(amountToBuffer(amountWithFee))
+ .put(bufferFromAmount(amountWithFee))
.put(denomPubHash)
.put(evHash)
.build();
@@ -1164,8 +1164,8 @@ export const nativeCryptoR: TalerCryptoInterfaceR = {
.put(hash(stringToBytes(type + "\0")))
.put(timestampRoundedToBuffer(wf.startStamp))
.put(timestampRoundedToBuffer(wf.endStamp))
- .put(amountToBuffer(wf.wireFee))
- .put(amountToBuffer(wf.closingFee))
+ .put(bufferFromAmount(wf.wireFee))
+ .put(bufferFromAmount(wf.closingFee))
.build();
const sig = decodeCrock(wf.sig);
const pub = decodeCrock(masterPub);
@@ -1185,9 +1185,9 @@ export const nativeCryptoR: TalerCryptoInterfaceR = {
.put(timestampRoundedToBuffer(gf.end_date))
.put(durationRoundedToBuffer(gf.purse_timeout))
.put(durationRoundedToBuffer(gf.history_expiration))
- .put(amountToBuffer(Amounts.parseOrThrow(gf.history_fee)))
- .put(amountToBuffer(Amounts.parseOrThrow(gf.account_fee)))
- .put(amountToBuffer(Amounts.parseOrThrow(gf.purse_fee)))
+ .put(bufferFromAmount(Amounts.parseOrThrow(gf.history_fee)))
+ .put(bufferFromAmount(Amounts.parseOrThrow(gf.account_fee)))
+ .put(bufferFromAmount(Amounts.parseOrThrow(gf.purse_fee)))
.put(bufferForUint32(gf.purse_account_limit))
.build();
const sig = decodeCrock(gf.master_sig);
@@ -1209,11 +1209,11 @@ export const nativeCryptoR: TalerCryptoInterfaceR = {
.put(timestampRoundedToBuffer(req.stampExpireWithdraw))
.put(timestampRoundedToBuffer(req.stampExpireDeposit))
.put(timestampRoundedToBuffer(req.stampExpireLegal))
- .put(amountToBuffer(value))
- .put(amountToBuffer(req.feeWithdraw))
- .put(amountToBuffer(req.feeDeposit))
- .put(amountToBuffer(req.feeRefresh))
- .put(amountToBuffer(req.feeRefund))
+ .put(bufferFromAmount(value))
+ .put(bufferFromAmount(req.feeWithdraw))
+ .put(bufferFromAmount(req.feeDeposit))
+ .put(bufferFromAmount(req.feeRefresh))
+ .put(bufferFromAmount(req.feeRefund))
.put(decodeCrock(req.denomPubHash))
.build();
const sig = decodeCrock(req.masterSig);
@@ -1404,8 +1404,8 @@ export const nativeCryptoR: TalerCryptoInterfaceR = {
.put(decodeCrock(depositInfo.denomPubHash))
.put(timestampRoundedToBuffer(depositInfo.timestamp))
.put(timestampRoundedToBuffer(depositInfo.refundDeadline))
- .put(amountToBuffer(depositInfo.spendAmount))
- .put(amountToBuffer(depositInfo.feeDeposit))
+ .put(bufferFromAmount(depositInfo.spendAmount))
+ .put(bufferFromAmount(depositInfo.feeDeposit))
.put(decodeCrock(depositInfo.merchantPub))
.put(walletDataHash)
.build();
@@ -1451,7 +1451,163 @@ export const nativeCryptoR: TalerCryptoInterfaceR = {
tci: TalerCryptoInterfaceR,
req: DeriveRefreshSessionRequestV2,
): Promise<DerivedRefreshSessionV2> {
- throw new Error("Function not implemented.");
+ const {
+ newCoinDenoms,
+ feeRefresh,
+ kappa,
+ meltCoinDenomPubHash,
+ meltCoinPriv,
+ meltCoinPub,
+ sessionPublicSeed,
+ } = req;
+
+ const currency = Amounts.currencyOf(newCoinDenoms[0].value);
+ let valueWithFee = Amounts.zeroOfCurrency(currency);
+
+ for (const ncd of newCoinDenoms) {
+ const t = Amounts.add(ncd.value, ncd.feeWithdraw).amount;
+ valueWithFee = Amounts.add(
+ valueWithFee,
+ Amounts.mult(t, ncd.count).amount,
+ ).amount;
+ }
+
+ const planchetsForGammas: RefreshPlanchetInfo[][] = [];
+
+ const noncesBytes = kdfKw({
+ outputLength: 64 * kappa,
+ salt: stringToBytes("refresh-kappa-nonces"),
+ ikm: decodeCrock(sessionPublicSeed),
+ });
+
+ const numDenoms = newCoinDenoms.length;
+ let numCoins = 0;
+ const coinDenomsHc = createHashContext();
+ for (let i = 0; i < numDenoms; i++) {
+ for (let j = 0; j < newCoinDenoms[i].count; j++) {
+ coinDenomsHc.update(decodeCrock(newCoinDenoms[i].denomPubHash));
+ numCoins++;
+ }
+ }
+ const coinDenomsHash = coinDenomsHc.finish();
+
+ const signatures: EddsaSignatureString[] = [];
+
+ const sessionHc = createHashContext();
+
+ sessionHc.update(decodeCrock(sessionPublicSeed));
+ sessionHc.update(decodeCrock(meltCoinPub));
+ sessionHc.update(bufferFromAmount(valueWithFee));
+
+ for (let i = 0; i < kappa; i++) {
+ const planchets: RefreshPlanchetInfo[] = [];
+ const nonce = noncesBytes.slice(i * 64, i * 64 + 64);
+
+ const coinLink = buildSigPS(TalerSignaturePurpose.WALLET_COIN_LINK)
+ .put(nonce)
+ .put(coinDenomsHash)
+ .put(bufferForUint32(i))
+ .build();
+ const sig = await tci.eddsaSign(tci, {
+ msg: encodeCrock(coinLink),
+ priv: meltCoinPriv,
+ });
+ signatures.push(sig.sig);
+
+ const planchetSecretBytes = kdfKw({
+ outputLength: 32 * numCoins,
+ salt: stringToBytes("refresh-planchet-secret"),
+ ikm: decodeCrock(sig.sig),
+ });
+
+ for (let j = 0; j < numDenoms; j++) {
+ const denomSel = newCoinDenoms[j];
+ for (let k = 0; k < newCoinDenoms[j].count; k++) {
+ const coinIndex = planchets.length;
+ const mySecret = planchetSecretBytes.slice(
+ 32 * coinIndex,
+ 32 * coinIndex + 32,
+ );
+ let fresh: FreshCoinEncoded = await tci.setupRefreshPlanchet(tci, {
+ coinNumber: coinIndex,
+ transferSecret: encodeCrock(mySecret),
+ });
+ let newAc: AgeCommitmentProof | undefined = undefined;
+ let newAch: HashCodeString | undefined = undefined;
+ if (req.meltCoinAgeCommitmentProof) {
+ newAc = await AgeRestriction.commitmentDerive(
+ req.meltCoinAgeCommitmentProof,
+ mySecret,
+ );
+ newAch = AgeRestriction.hashCommitment(newAc.commitment);
+ }
+ const coinPubHash = hashCoinPub(fresh.coinPub, newAch);
+ if (denomSel.denomPub.cipher !== DenomKeyType.Rsa) {
+ throw Error("unsupported cipher, can't create refresh session");
+ }
+ const blindResult = await tci.rsaBlind(tci, {
+ bks: fresh.bks,
+ hm: encodeCrock(coinPubHash),
+ pub: denomSel.denomPub.rsa_public_key,
+ });
+ const coinEv: CoinEnvelope = {
+ cipher: DenomKeyType.Rsa,
+ rsa_blinded_planchet: blindResult.blinded,
+ };
+ const coinEvHash = hashCoinEv(
+ coinEv,
+ encodeCrock(hashDenomPub(denomSel.denomPub)),
+ );
+ const planchet: RefreshPlanchetInfo = {
+ blindingKey: fresh.bks,
+ coinEv,
+ coinPriv: fresh.coinPriv,
+ coinPub: fresh.coinPub,
+ coinEvHash: encodeCrock(coinEvHash),
+ maxAge: req.meltCoinMaxAge,
+ ageCommitmentProof: newAc,
+ };
+ planchets.push(planchet);
+ sessionHc.update(coinEvHash);
+ }
+ }
+
+ planchetsForGammas.push(planchets);
+ }
+
+ const sessionHash = sessionHc.finish();
+ let confirmData: Uint8Array;
+ let hAgeCommitment: Uint8Array;
+ if (req.meltCoinAgeCommitmentProof) {
+ hAgeCommitment = decodeCrock(
+ AgeRestriction.hashCommitment(
+ req.meltCoinAgeCommitmentProof.commitment,
+ ),
+ );
+ } else {
+ hAgeCommitment = new Uint8Array(32);
+ }
+ confirmData = buildSigPS(TalerSignaturePurpose.WALLET_COIN_MELT)
+ .put(sessionHash)
+ .put(decodeCrock(meltCoinDenomPubHash))
+ .put(hAgeCommitment)
+ .put(bufferFromAmount(valueWithFee))
+ .put(bufferFromAmount(feeRefresh))
+ .build();
+
+ const confirmSigResp = await tci.eddsaSign(tci, {
+ msg: encodeCrock(confirmData),
+ priv: meltCoinPriv,
+ });
+
+ return {
+ signatures,
+ planchets: planchetsForGammas,
+ confirmSig: confirmSigResp.sig,
+ hash: encodeCrock(sessionHash),
+ meltCoinPub: meltCoinPub,
+ meltValueWithFee: valueWithFee,
+ };
},
async deriveRefreshSession(
@@ -1511,7 +1667,7 @@ export const nativeCryptoR: TalerCryptoInterfaceR = {
}
sessionHc.update(decodeCrock(meltCoinPub));
- sessionHc.update(amountToBuffer(valueWithFee));
+ sessionHc.update(bufferFromAmount(valueWithFee));
for (let i = 0; i < kappa; i++) {
const planchets: RefreshPlanchetInfo[] = [];
@@ -1591,8 +1747,8 @@ export const nativeCryptoR: TalerCryptoInterfaceR = {
.put(sessionHash)
.put(decodeCrock(meltCoinDenomPubHash))
.put(hAgeCommitment)
- .put(amountToBuffer(valueWithFee))
- .put(amountToBuffer(meltFee))
+ .put(bufferFromAmount(valueWithFee))
+ .put(bufferFromAmount(meltFee))
.build();
const confirmSigResp = await tci.eddsaSign(tci, {
@@ -1704,7 +1860,7 @@ export const nativeCryptoR: TalerCryptoInterfaceR = {
): Promise<EddsaSigningResult> {
const sigBlob = buildSigPS(TalerSignaturePurpose.WALLET_PURSE_CREATE)
.put(timestampRoundedToBuffer(req.purseExpiration))
- .put(amountToBuffer(Amounts.parseOrThrow(req.purseAmount)))
+ .put(bufferFromAmount(Amounts.parseOrThrow(req.purseAmount)))
.put(decodeCrock(req.hContractTerms))
.put(decodeCrock(req.mergePub))
.put(bufferForUint32(req.minAge))
@@ -1730,7 +1886,7 @@ export const nativeCryptoR: TalerCryptoInterfaceR = {
maybeAch = new Uint8Array(32);
}
const sigBlob = buildSigPS(TalerSignaturePurpose.WALLET_PURSE_DEPOSIT)
- .put(amountToBuffer(Amounts.parseOrThrow(c.contribution)))
+ .put(bufferFromAmount(Amounts.parseOrThrow(c.contribution)))
.put(decodeCrock(c.denomPubHash))
.put(maybeAch)
.put(decodeCrock(req.pursePub))
@@ -1847,8 +2003,8 @@ export const nativeCryptoR: TalerCryptoInterfaceR = {
TalerSignaturePurpose.WALLET_ACCOUNT_MERGE,
)
.put(timestampRoundedToBuffer(req.purseExpiration))
- .put(amountToBuffer(Amounts.parseOrThrow(req.purseAmount)))
- .put(amountToBuffer(Amounts.parseOrThrow(req.purseFee)))
+ .put(bufferFromAmount(Amounts.parseOrThrow(req.purseAmount)))
+ .put(bufferFromAmount(Amounts.parseOrThrow(req.purseFee)))
.put(decodeCrock(req.contractTermsHash))
.put(decodeCrock(req.pursePub))
.put(timestampRoundedToBuffer(req.mergeTimestamp))
@@ -1892,8 +2048,8 @@ export const nativeCryptoR: TalerCryptoInterfaceR = {
TalerSignaturePurpose.WALLET_ACCOUNT_MERGE,
)
.put(timestampRoundedToBuffer(req.purseExpiration))
- .put(amountToBuffer(Amounts.parseOrThrow(req.purseAmount)))
- .put(amountToBuffer(Amounts.parseOrThrow(req.purseFee)))
+ .put(bufferFromAmount(Amounts.parseOrThrow(req.purseAmount)))
+ .put(bufferFromAmount(Amounts.parseOrThrow(req.purseFee)))
.put(decodeCrock(req.contractTermsHash))
.put(decodeCrock(req.pursePub))
.put(timestampRoundedToBuffer(req.mergeTimestamp))
@@ -1915,7 +2071,7 @@ export const nativeCryptoR: TalerCryptoInterfaceR = {
const purseSigBlob = buildSigPS(TalerSignaturePurpose.WALLET_PURSE_CREATE)
.put(timestampRoundedToBuffer(req.purseExpiration))
- .put(amountToBuffer(Amounts.parseOrThrow(req.purseAmount)))
+ .put(bufferFromAmount(Amounts.parseOrThrow(req.purseAmount)))
.put(decodeCrock(req.contractTermsHash))
.put(decodeCrock(mergePub))
// FIXME: add age!
@@ -1941,7 +2097,7 @@ export const nativeCryptoR: TalerCryptoInterfaceR = {
.put(decodeCrock(req.contractTermsHash))
.put(decodeCrock(req.coinPub))
.put(bufferForUint64(req.rtransactionId))
- .put(amountToBuffer(req.refundAmount))
+ .put(bufferFromAmount(req.refundAmount))
.build();
const refundSigResp = await tci.eddsaSign(tci, {
msg: encodeCrock(refundSigBlob),
@@ -2005,7 +2161,7 @@ export const nativeCryptoR: TalerCryptoInterfaceR = {
req: SignWalletAccountSetupRequest,
): Promise<SignWalletAccountSetupResponse> {
const sigData = buildSigPS(TalerSignaturePurpose.WALLET_ACCOUNT_SETUP)
- .put(amountToBuffer(req.threshold))
+ .put(bufferFromAmount(req.threshold))
.build();
const sigResp = await tci.eddsaSign(tci, {
msg: encodeCrock(sigData),
@@ -2059,8 +2215,8 @@ export const nativeCryptoR: TalerCryptoInterfaceR = {
const withdrawRequest = buildSigPS(
TalerSignaturePurpose.WALLET_RESERVE_WITHDRAW,
)
- .put(amountToBuffer(req.amount))
- .put(amountToBuffer(req.fee))
+ .put(bufferFromAmount(req.amount))
+ .put(bufferFromAmount(req.fee))
.put(hPlanchets)
.put(bufferForUint32(0)) // max_age_group
.put(bufferForUint32(0)) // age mask
diff --git a/packages/taler-wallet-core/src/refresh.ts b/packages/taler-wallet-core/src/refresh.ts
@@ -770,9 +770,7 @@ async function refreshMelt(
},
);
if (!updatedSession) {
- throw Error(
- "Could not update refresh session (concurrent deletion?).",
- );
+ throw Error("Could not update refresh session (concurrent deletion?).");
}
d.refreshSession = updatedSession;
}
@@ -924,6 +922,7 @@ async function refreshMelt(
denoms_h: newCoinDenoms.map((x) => x.denomPubHash),
value_with_fee: Amounts.stringify(derived.meltValueWithFee),
};
+ logger.info(`melt request body: ${j2s(meltReqBody)}`);
const resp = await wex.ws.runSequentialized(
[EXCHANGE_COINS_LOCK],
async () => {