From f63765b9f7a089eb0f2a62d53f5ad1d56961fa1f Mon Sep 17 00:00:00 2001 From: Florian Dold Date: Mon, 19 Sep 2022 17:08:04 +0200 Subject: wallet-core: fix tipping with age restricted denoms --- packages/taler-util/src/talerCrypto.ts | 55 +++++++++++++- packages/taler-wallet-cli/src/harness/harness.ts | 3 +- .../test-age-restrictions-merchant.ts | 85 +++++++++++++++++++--- .../src/integrationtests/test-tipping.ts | 4 +- .../src/crypto/cryptoImplementation.ts | 12 ++- .../taler-wallet-core/src/crypto/cryptoTypes.ts | 1 + packages/taler-wallet-core/src/operations/tip.ts | 1 + 7 files changed, 144 insertions(+), 17 deletions(-) (limited to 'packages') diff --git a/packages/taler-util/src/talerCrypto.ts b/packages/taler-util/src/talerCrypto.ts index c9eeb0584..28fdab8e3 100644 --- a/packages/taler-util/src/talerCrypto.ts +++ b/packages/taler-util/src/talerCrypto.ts @@ -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 { 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 { + 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 */ diff --git a/packages/taler-wallet-cli/src/harness/harness.ts b/packages/taler-wallet-cli/src/harness/harness.ts index ca0ea1f2f..137027964 100644 --- a/packages/taler-wallet-cli/src/harness/harness.ts +++ b/packages/taler-wallet-cli/src/harness/harness.ts @@ -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"; } diff --git a/packages/taler-wallet-cli/src/integrationtests/test-age-restrictions-merchant.ts b/packages/taler-wallet-cli/src/integrationtests/test-age-restrictions-merchant.ts index 01ddac4d9..ff589dd79 100644 --- a/packages/taler-wallet-cli/src/integrationtests/test-age-restrictions-merchant.ts +++ b/packages/taler-wallet-cli/src/integrationtests/test-age-restrictions-merchant.ts @@ -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; diff --git a/packages/taler-wallet-cli/src/integrationtests/test-tipping.ts b/packages/taler-wallet-cli/src/integrationtests/test-tipping.ts index f04293ed8..d31e0c06b 100644 --- a/packages/taler-wallet-cli/src/integrationtests/test-tipping.ts +++ b/packages/taler-wallet-cli/src/integrationtests/test-tipping.ts @@ -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(), }, ); diff --git a/packages/taler-wallet-core/src/crypto/cryptoImplementation.ts b/packages/taler-wallet-core/src/crypto/cryptoImplementation.ts index c21ee99e8..bfc48d961 100644 --- a/packages/taler-wallet-core/src/crypto/cryptoImplementation.ts +++ b/packages/taler-wallet-core/src/crypto/cryptoImplementation.ts @@ -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; }, diff --git a/packages/taler-wallet-core/src/crypto/cryptoTypes.ts b/packages/taler-wallet-core/src/crypto/cryptoTypes.ts index 4c75aa91e..0858cffa9 100644 --- a/packages/taler-wallet-core/src/crypto/cryptoTypes.ts +++ b/packages/taler-wallet-core/src/crypto/cryptoTypes.ts @@ -122,6 +122,7 @@ export interface DerivedTipPlanchet { coinEvHash: string; coinPriv: string; coinPub: string; + ageCommitmentProof: AgeCommitmentProof | undefined; } export interface SignTrackTransactionRequest { diff --git a/packages/taler-wallet-core/src/operations/tip.ts b/packages/taler-wallet-core/src/operations/tip.ts index 571721658..a0fd8d328 100644 --- a/packages/taler-wallet-core/src/operations/tip.ts +++ b/packages/taler-wallet-core/src/operations/tip.ts @@ -319,6 +319,7 @@ export async function processTip( status: CoinStatus.Fresh, coinEvHash: planchet.coinEvHash, maxAge: AgeRestriction.AGE_UNRESTRICTED, + ageCommitmentProof: planchet.ageCommitmentProof, }); } -- cgit v1.2.3