wallet-core: fix tipping with age restricted denoms
This commit is contained in:
parent
ffe6a95214
commit
f63765b9f7
@ -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
|
||||||
*/
|
*/
|
||||||
|
@ -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";
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
|
@ -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(),
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -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;
|
||||||
},
|
},
|
||||||
|
@ -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 {
|
||||||
|
@ -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,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user