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;
coinPriv: Uint8Array;
bks: Uint8Array;
maxAge: number;
ageCommitmentProof: AgeCommitmentProof | undefined;
}
export function bufferForUint32(n: number): Uint8Array {
@ -742,10 +744,11 @@ export function bufferForUint8(n: number): Uint8Array {
return buf;
}
export function setupTipPlanchet(
export async function setupTipPlanchet(
secretSeed: Uint8Array,
denomPub: DenominationPubKey,
coinNumber: number,
): FreshCoin {
): Promise<FreshCoin> {
const info = stringToBytes("taler-tip-coin-derivation");
const saltArrBuf = new ArrayBuffer(4);
const salt = new Uint8Array(saltArrBuf);
@ -754,10 +757,20 @@ export function setupTipPlanchet(
const out = kdf(64, secretSeed, salt, info);
const coinPriv = out.slice(0, 32);
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 {
bks,
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
*/

View File

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

View File

@ -17,8 +17,14 @@
/**
* Imports.
*/
import { BankApi, WalletApiOperation } from "@gnu-taler/taler-wallet-core";
import { defaultCoinConfig } from "../harness/denomStructures.js";
import { GlobalTestState, WalletCli } from "../harness/harness.js";
import {
getWireMethodForTest,
GlobalTestState,
MerchantPrivateApi,
WalletCli,
} from "../harness/harness.js";
import {
createSimpleTestkudosEnvironment,
withdrawViaBank,
@ -31,14 +37,19 @@ import {
export async function runAgeRestrictionsMerchantTest(t: GlobalTestState) {
// Set up test environment
const { wallet: walletOne, bank, exchange, merchant } =
await createSimpleTestkudosEnvironment(
t,
defaultCoinConfig.map((x) => x("TESTKUDOS")),
{
ageMaskSpec: "8:10:12:14:16:18:21",
},
);
const {
wallet: walletOne,
bank,
exchange,
merchant,
exchangeBankAccount,
} = await createSimpleTestkudosEnvironment(
t,
defaultCoinConfig.map((x) => x("TESTKUDOS")),
{
ageMaskSpec: "8:10:12:14:16:18:21",
},
);
const walletTwo = new WalletCli(t, "walletTwo");
const walletThree = new WalletCli(t, "walletThree");
@ -129,6 +140,62 @@ export async function runAgeRestrictionsMerchantTest(t: GlobalTestState) {
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.timeoutMs = 120 * 1000;

View File

@ -21,7 +21,7 @@ import { WalletApiOperation, BankApi } from "@gnu-taler/taler-wallet-core";
import {
GlobalTestState,
MerchantPrivateApi,
getWireMethod,
getWireMethodForTest,
} from "../harness/harness.js";
import { createSimpleTestkudosEnvironment } from "../harness/helpers.js";
@ -42,7 +42,7 @@ export async function runTippingTest(t: GlobalTestState) {
{
exchange_url: exchange.baseUrl,
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) {
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 coinPubHash = hash(fc.coinPub);
const coinPubHash = hashCoinPub(encodeCrock(fc.coinPub), maybeAch);
const blindResp = await tci.rsaBlind(tci, {
bks: encodeCrock(fc.bks),
hm: encodeCrock(coinPubHash),
@ -763,6 +770,7 @@ export const nativeCryptoR: TalerCryptoInterfaceR = {
),
coinPriv: encodeCrock(fc.coinPriv),
coinPub: encodeCrock(fc.coinPub),
ageCommitmentProof: fc.ageCommitmentProof,
};
return tipPlanchet;
},

View File

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

View File

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