wallet-core: fix tipping with age restricted denoms

This commit is contained in:
Florian Dold 2022-09-19 17:08:04 +02:00
parent ffe6a95214
commit f63765b9f7
No known key found for this signature in database
GPG Key ID: D2E4F00F29D02A4B
7 changed files with 144 additions and 17 deletions

View File

@ -724,6 +724,8 @@ export interface FreshCoin {
coinPub: Uint8Array; coinPub: Uint8Array;
coinPriv: Uint8Array; coinPriv: Uint8Array;
bks: Uint8Array; bks: Uint8Array;
maxAge: number;
ageCommitmentProof: AgeCommitmentProof | undefined;
} }
export function bufferForUint32(n: number): Uint8Array { export function bufferForUint32(n: number): Uint8Array {
@ -742,10 +744,11 @@ export function bufferForUint8(n: number): Uint8Array {
return buf; return buf;
} }
export function setupTipPlanchet( export async function setupTipPlanchet(
secretSeed: Uint8Array, secretSeed: Uint8Array,
denomPub: DenominationPubKey,
coinNumber: number, coinNumber: number,
): FreshCoin { ): Promise<FreshCoin> {
const info = stringToBytes("taler-tip-coin-derivation"); const info = stringToBytes("taler-tip-coin-derivation");
const saltArrBuf = new ArrayBuffer(4); const saltArrBuf = new ArrayBuffer(4);
const salt = new Uint8Array(saltArrBuf); const salt = new Uint8Array(saltArrBuf);
@ -754,10 +757,20 @@ export function setupTipPlanchet(
const out = kdf(64, secretSeed, salt, info); const out = kdf(64, secretSeed, salt, info);
const coinPriv = out.slice(0, 32); const coinPriv = out.slice(0, 32);
const bks = out.slice(32, 64); const bks = out.slice(32, 64);
let maybeAcp: AgeCommitmentProof | undefined;
if (denomPub.age_mask != 0) {
maybeAcp = await AgeRestriction.restrictionCommitSeeded(
denomPub.age_mask,
AgeRestriction.AGE_UNRESTRICTED,
secretSeed,
);
}
return { return {
bks, bks,
coinPriv, coinPriv,
coinPub: eddsaGetPublic(coinPriv), coinPub: eddsaGetPublic(coinPriv),
maxAge: AgeRestriction.AGE_UNRESTRICTED,
ageCommitmentProof: maybeAcp,
}; };
} }
/** /**
@ -1062,6 +1075,44 @@ export namespace AgeRestriction {
}; };
} }
export async function restrictionCommitSeeded(
ageMask: number,
age: number,
seed: Uint8Array,
): Promise<AgeCommitmentProof> {
invariant((ageMask & 1) === 1);
const numPubs = countAgeGroups(ageMask) - 1;
const numPrivs = getAgeGroupIndex(ageMask, age);
const pubs: Edx25519PublicKey[] = [];
const privs: Edx25519PrivateKey[] = [];
for (let i = 0; i < numPubs; i++) {
const privSeed = await kdfKw({
outputLength: 32,
ikm: seed,
info: stringToBytes("age-restriction-commit"),
salt: bufferForUint32(i),
});
const priv = await Edx25519.keyCreateFromSeed(privSeed);
const pub = await Edx25519.getPublic(priv);
pubs.push(pub);
if (i < numPrivs) {
privs.push(priv);
}
}
return {
commitment: {
mask: ageMask,
publicKeys: pubs.map((x) => encodeCrock(x)),
},
proof: {
privateKeys: privs.map((x) => encodeCrock(x)),
},
};
}
/** /**
* Check that c1 = c2*salt * Check that c1 = c2*salt
*/ */

View File

@ -1991,8 +1991,7 @@ export function getRandomIban(salt: string | null = null): string {
return `DE${check_digits}${bban}`; return `DE${check_digits}${bban}`;
} }
// Only used in one tipping test. export function getWireMethodForTest(): string {
export function getWireMethod(): string {
if (useLibeufinBank) return "iban"; if (useLibeufinBank) return "iban";
return "x-taler-bank"; return "x-taler-bank";
} }

View File

@ -17,8 +17,14 @@
/** /**
* Imports. * Imports.
*/ */
import { BankApi, WalletApiOperation } from "@gnu-taler/taler-wallet-core";
import { defaultCoinConfig } from "../harness/denomStructures.js"; import { defaultCoinConfig } from "../harness/denomStructures.js";
import { GlobalTestState, WalletCli } from "../harness/harness.js"; import {
getWireMethodForTest,
GlobalTestState,
MerchantPrivateApi,
WalletCli,
} from "../harness/harness.js";
import { import {
createSimpleTestkudosEnvironment, createSimpleTestkudosEnvironment,
withdrawViaBank, withdrawViaBank,
@ -31,8 +37,13 @@ import {
export async function runAgeRestrictionsMerchantTest(t: GlobalTestState) { export async function runAgeRestrictionsMerchantTest(t: GlobalTestState) {
// Set up test environment // Set up test environment
const { wallet: walletOne, bank, exchange, merchant } = const {
await createSimpleTestkudosEnvironment( wallet: walletOne,
bank,
exchange,
merchant,
exchangeBankAccount,
} = await createSimpleTestkudosEnvironment(
t, t,
defaultCoinConfig.map((x) => x("TESTKUDOS")), defaultCoinConfig.map((x) => x("TESTKUDOS")),
{ {
@ -129,6 +140,62 @@ export async function runAgeRestrictionsMerchantTest(t: GlobalTestState) {
await wallet.runUntilDone(); await wallet.runUntilDone();
} }
// Pay with coin from tipping
{
const mbu = await BankApi.createRandomBankUser(bank);
const tipReserveResp = await MerchantPrivateApi.createTippingReserve(
merchant,
"default",
{
exchange_url: exchange.baseUrl,
initial_balance: "TESTKUDOS:10",
wire_method: getWireMethodForTest(),
},
);
t.assertDeepEqual(
tipReserveResp.payto_uri,
exchangeBankAccount.accountPaytoUri,
);
await BankApi.adminAddIncoming(bank, {
amount: "TESTKUDOS:10",
debitAccountPayto: mbu.accountPaytoUri,
exchangeBankAccount,
reservePub: tipReserveResp.reserve_pub,
});
await exchange.runWirewatchOnce();
const tip = await MerchantPrivateApi.giveTip(merchant, "default", {
amount: "TESTKUDOS:5",
justification: "why not?",
next_url: "https://example.com/after-tip",
});
const walletTipping = new WalletCli(t, "age-tipping");
const ptr = await walletTipping.client.call(WalletApiOperation.PrepareTip, {
talerTipUri: tip.taler_tip_uri,
});
await walletTipping.client.call(WalletApiOperation.AcceptTip, {
walletTipId: ptr.walletTipId,
});
await walletTipping.runUntilDone();
const order = {
summary: "Buy me!",
amount: "TESTKUDOS:4",
fulfillment_url: "taler://fulfillment-success/thx",
minimum_age: 9,
};
await makeTestPayment(t, { wallet: walletTipping, merchant, order });
await walletTipping.runUntilDone();
}
} }
runAgeRestrictionsMerchantTest.suites = ["wallet"]; runAgeRestrictionsMerchantTest.suites = ["wallet"];
runAgeRestrictionsMerchantTest.timeoutMs = 120 * 1000;

View File

@ -21,7 +21,7 @@ import { WalletApiOperation, BankApi } from "@gnu-taler/taler-wallet-core";
import { import {
GlobalTestState, GlobalTestState,
MerchantPrivateApi, MerchantPrivateApi,
getWireMethod, getWireMethodForTest,
} from "../harness/harness.js"; } from "../harness/harness.js";
import { createSimpleTestkudosEnvironment } from "../harness/helpers.js"; import { createSimpleTestkudosEnvironment } from "../harness/helpers.js";
@ -42,7 +42,7 @@ export async function runTippingTest(t: GlobalTestState) {
{ {
exchange_url: exchange.baseUrl, exchange_url: exchange.baseUrl,
initial_balance: "TESTKUDOS:10", initial_balance: "TESTKUDOS:10",
wire_method: getWireMethod(), wire_method: getWireMethodForTest(),
}, },
); );

View File

@ -743,9 +743,16 @@ export const nativeCryptoR: TalerCryptoInterfaceR = {
if (req.denomPub.cipher !== DenomKeyType.Rsa) { if (req.denomPub.cipher !== DenomKeyType.Rsa) {
throw Error(`unsupported cipher (${req.denomPub.cipher})`); throw Error(`unsupported cipher (${req.denomPub.cipher})`);
} }
const fc = setupTipPlanchet(decodeCrock(req.secretSeed), req.planchetIndex); const fc = await setupTipPlanchet(
decodeCrock(req.secretSeed),
req.denomPub,
req.planchetIndex,
);
const maybeAch = fc.ageCommitmentProof
? AgeRestriction.hashCommitment(fc.ageCommitmentProof.commitment)
: undefined;
const denomPub = decodeCrock(req.denomPub.rsa_public_key); const denomPub = decodeCrock(req.denomPub.rsa_public_key);
const coinPubHash = hash(fc.coinPub); const coinPubHash = hashCoinPub(encodeCrock(fc.coinPub), maybeAch);
const blindResp = await tci.rsaBlind(tci, { const blindResp = await tci.rsaBlind(tci, {
bks: encodeCrock(fc.bks), bks: encodeCrock(fc.bks),
hm: encodeCrock(coinPubHash), hm: encodeCrock(coinPubHash),
@ -763,6 +770,7 @@ export const nativeCryptoR: TalerCryptoInterfaceR = {
), ),
coinPriv: encodeCrock(fc.coinPriv), coinPriv: encodeCrock(fc.coinPriv),
coinPub: encodeCrock(fc.coinPub), coinPub: encodeCrock(fc.coinPub),
ageCommitmentProof: fc.ageCommitmentProof,
}; };
return tipPlanchet; return tipPlanchet;
}, },

View File

@ -122,6 +122,7 @@ export interface DerivedTipPlanchet {
coinEvHash: string; coinEvHash: string;
coinPriv: string; coinPriv: string;
coinPub: string; coinPub: string;
ageCommitmentProof: AgeCommitmentProof | undefined;
} }
export interface SignTrackTransactionRequest { export interface SignTrackTransactionRequest {

View File

@ -319,6 +319,7 @@ export async function processTip(
status: CoinStatus.Fresh, status: CoinStatus.Fresh,
coinEvHash: planchet.coinEvHash, coinEvHash: planchet.coinEvHash,
maxAge: AgeRestriction.AGE_UNRESTRICTED, maxAge: AgeRestriction.AGE_UNRESTRICTED,
ageCommitmentProof: planchet.ageCommitmentProof,
}); });
} }