aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--packages/taler-util/src/talerCrypto.ts55
-rw-r--r--packages/taler-wallet-cli/src/harness/harness.ts3
-rw-r--r--packages/taler-wallet-cli/src/integrationtests/test-age-restrictions-merchant.ts85
-rw-r--r--packages/taler-wallet-cli/src/integrationtests/test-tipping.ts4
-rw-r--r--packages/taler-wallet-core/src/crypto/cryptoImplementation.ts12
-rw-r--r--packages/taler-wallet-core/src/crypto/cryptoTypes.ts1
-rw-r--r--packages/taler-wallet-core/src/operations/tip.ts1
7 files changed, 144 insertions, 17 deletions
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<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
*/
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,
});
}