wallet-core: implement age restriction support

This commit is contained in:
Florian Dold 2022-04-19 17:12:43 +02:00
parent 9b85d139bf
commit a165afa682
No known key found for this signature in database
GPG Key ID: D2E4F00F29D02A4B
22 changed files with 631 additions and 67 deletions

View File

@ -1769,7 +1769,7 @@ function crypto_scalarmult_base(q: Uint8Array, n: Uint8Array): number {
return crypto_scalarmult(q, n, _9);
}
function crypto_scalarmult_noclamp(
export function crypto_scalarmult_noclamp(
q: Uint8Array,
n: Uint8Array,
p: Uint8Array,
@ -3033,6 +3033,18 @@ export function crypto_core_ed25519_scalar_add(
return o;
}
/**
* Reduce a scalar "s" to "s mod L". The input can be up to 64 bytes long.
*/
export function crypto_core_ed25519_scalar_reduce(x: Uint8Array): Uint8Array {
const len = x.length;
const z = new Float64Array(64);
for (let i = 0; i < len; i++) z[i] = x[i];
const o = new Uint8Array(32);
modL(o, z);
return o;
}
export function crypto_core_ed25519_scalar_sub(
x: Uint8Array,
y: Uint8Array,
@ -3063,11 +3075,7 @@ export function crypto_edx25519_private_key_create_from_seed(
}
export function crypto_edx25519_get_public(priv: Uint8Array): Uint8Array {
const pub = new Uint8Array(32);
if (0 != crypto_scalarmult_base_noclamp(pub.subarray(32), priv)) {
throw Error();
}
return pub;
return crypto_scalarmult_ed25519_base_noclamp(priv.subarray(0, 32));
}
export function crypto_edx25519_sign_detached(
@ -3076,19 +3084,16 @@ export function crypto_edx25519_sign_detached(
pkx: Uint8Array,
): Uint8Array {
const n: number = m.length;
const d = new Uint8Array(64),
h = new Uint8Array(64),
r = new Uint8Array(64);
const h = new Uint8Array(64);
const r = new Uint8Array(64);
let i, j;
const x = new Float64Array(64);
const p = [gf(), gf(), gf(), gf()];
for (i = 0; i < 64; i++) d[i] = skx[i];
const sm = new Uint8Array(n + 64);
for (i = 0; i < n; i++) sm[64 + i] = m[i];
for (i = 0; i < 32; i++) sm[32 + i] = d[32 + i];
for (i = 0; i < 32; i++) sm[32 + i] = skx[32 + i];
crypto_hash(r, sm.subarray(32), n + 32);
reduce(r);
@ -3103,12 +3108,12 @@ export function crypto_edx25519_sign_detached(
for (i = 0; i < 32; i++) x[i] = r[i];
for (i = 0; i < 32; i++) {
for (j = 0; j < 32; j++) {
x[i + j] += h[i] * d[j];
x[i + j] += h[i] * skx[j];
}
}
modL(sm.subarray(32), x);
return sm.subarray(64);
return sm.subarray(0, 64);
}
export function crypto_edx25519_sign_detached_verify(

View File

@ -34,6 +34,10 @@ import {
scalarMultBase25519,
deriveSecrets,
calcRBlind,
Edx25519,
getRandomBytes,
bigintToNaclArr,
bigintFromNaclArr,
} from "./talerCrypto.js";
import { sha512, kdf } from "./kdf.js";
import * as nacl from "./nacl-fast.js";
@ -44,6 +48,7 @@ import { initNodePrng } from "./prng-node.js";
initNodePrng();
import bigint from "big-integer";
import { AssertionError } from "assert";
import BigInteger from "big-integer";
test("encoding", (t) => {
const s = "Hello, World";
@ -343,9 +348,86 @@ test("taler CS blind c", async (t) => {
};
const sig = await csUnblind(bseed, rPub, pub, b, blindsig);
t.deepEqual(sig.s, decodeCrock("F4ZKMFW3Q7DFN0N94KAMG2JFFHAC362T0QZ6ZCVZ73RS8P91CR70"));
t.deepEqual(sig.rPub, decodeCrock("CHK7JC4SXZ4Y9RDA3881S82F7BP99H35Q361WR6RBXN5YN2ZM1M0"));
t.deepEqual(
sig.s,
decodeCrock("F4ZKMFW3Q7DFN0N94KAMG2JFFHAC362T0QZ6ZCVZ73RS8P91CR70"),
);
t.deepEqual(
sig.rPub,
decodeCrock("CHK7JC4SXZ4Y9RDA3881S82F7BP99H35Q361WR6RBXN5YN2ZM1M0"),
);
const res = await csVerify(decodeCrock(msg_hash), sig, pub);
t.deepEqual(res, true);
});
test("bigint/nacl conversion", async (t) => {
const b1 = BigInteger(42);
const n1 = bigintToNaclArr(b1, 32);
t.is(n1[0], 42);
t.is(n1.length, 32);
const b2 = bigintFromNaclArr(n1);
t.true(b1.eq(b2));
});
test("taler age restriction crypto", async (t) => {
const priv1 = await Edx25519.keyCreate();
const pub1 = await Edx25519.getPublic(priv1);
const seed = encodeCrock(getRandomBytes(32));
const priv2 = await Edx25519.privateKeyDerive(priv1, seed);
const pub2 = await Edx25519.publicKeyDerive(pub1, seed);
const pub2Ref = await Edx25519.getPublic(priv2);
t.is(pub2, pub2Ref);
});
test("edx signing", async (t) => {
const priv1 = await Edx25519.keyCreate();
const pub1 = await Edx25519.getPublic(priv1);
const msg = stringToBytes("hello world");
const sig = nacl.crypto_edx25519_sign_detached(
msg,
decodeCrock(priv1),
decodeCrock(pub1),
);
t.true(
nacl.crypto_edx25519_sign_detached_verify(msg, sig, decodeCrock(pub1)),
);
sig[0]++;
t.false(
nacl.crypto_edx25519_sign_detached_verify(msg, sig, decodeCrock(pub1)),
);
});
test("edx test vector", async (t) => {
// Generated by gnunet-crypto-tvg
const tv = {
operation: "edx25519_derive",
priv1_edx:
"216KF1XM46K4JN8TX3Z8HNRX1DX4WRMX1BTCQM3KBS83PYKFY1GV6XRNBYRC5YM02HVDX8BDR20V7A27YX4MZJ8X8K0ADPZ43BD1GXG",
pub1_edx: "RKGRRG74SZ8PKF8SYG5SSDY8VRCYYGY5N2AKAJCG0103Z3JK6HTG",
seed: "EFK7CYT98YWGPNZNHPP84VJZDMXD5A41PP3E94NSAQZXRCAKVVXHAQNXG9XM2MAND2FJ56ZM238KGDCF3B0KCWNZCYKKHKDB56X6QA0",
priv2_edx:
"JRV3S06REHQV90E4HJA1FAMCVDBZZAZP9C6N2WF01MSR3CD5KM28QM7HTGGAV6MBJZ73QJ8PSZFA0D6YENJ7YT97344FDVVCGVAFNER",
pub2_edx: "ZB546ZC7ZP16DB99AMK67WNZ67WZFPWMRY67Y4PZR9YR1D82GVZ0",
};
{
const pub1Prime = await Edx25519.getPublic(tv.priv1_edx);
t.is(pub1Prime, tv.pub1_edx);
}
const pub2Prime = await Edx25519.publicKeyDerive(tv.pub1_edx, tv.seed);
t.is(pub2Prime, tv.pub2_edx);
const priv2Prime = await Edx25519.privateKeyDerive(tv.priv1_edx, tv.seed);
t.is(priv2Prime, tv.priv2_edx);
});

View File

@ -27,6 +27,7 @@ import bigint from "big-integer";
import {
Base32String,
CoinEnvelope,
CoinPublicKeyString,
DenominationPubKey,
DenomKeyType,
HashCodeString,
@ -643,6 +644,17 @@ export function hashCoinEvInner(
}
}
export function hashCoinPub(
coinPub: CoinPublicKeyString,
ach?: HashCodeString,
): Uint8Array {
if (!ach) {
return hash(decodeCrock(coinPub));
}
return hash(typedArrayConcat([decodeCrock(coinPub), decodeCrock(ach)]));
}
/**
* Hash a denomination public key.
*/
@ -652,6 +664,7 @@ export function hashDenomPub(pub: DenominationPubKey): Uint8Array {
const hashInputBuf = new ArrayBuffer(pubBuf.length + 4 + 4);
const uint8ArrayBuf = new Uint8Array(hashInputBuf);
const dv = new DataView(hashInputBuf);
logger.info("age_mask", pub.age_mask);
dv.setUint32(0, pub.age_mask ?? 0);
dv.setUint32(4, DenomKeyType.toIntTag(pub.cipher));
uint8ArrayBuf.set(pubBuf, 8);
@ -705,6 +718,14 @@ export function bufferForUint32(n: number): Uint8Array {
return buf;
}
export function bufferForUint8(n: number): Uint8Array {
const arrBuf = new ArrayBuffer(1);
const buf = new Uint8Array(arrBuf);
const dv = new DataView(arrBuf);
dv.setUint8(0, n);
return buf;
}
export function setupTipPlanchet(
secretSeed: Uint8Array,
coinNumber: number,
@ -753,6 +774,7 @@ export enum TalerSignaturePurpose {
WALLET_COIN_RECOUP = 1203,
WALLET_COIN_LINK = 1204,
WALLET_COIN_RECOUP_REFRESH = 1206,
WALLET_AGE_ATTESTATION = 1207,
EXCHANGE_CONFIRM_RECOUP = 1039,
EXCHANGE_CONFIRM_RECOUP_REFRESH = 1041,
ANASTASIS_POLICY_UPLOAD = 1400,
@ -807,6 +829,25 @@ export type Edx25519PublicKey = FlavorP<string, "Edx25519PublicKey", 32>;
export type Edx25519PrivateKey = FlavorP<string, "Edx25519PrivateKey", 64>;
export type Edx25519Signature = FlavorP<string, "Edx25519Signature", 64>;
/**
* Convert a big integer to a fixed-size, little-endian array.
*/
export function bigintToNaclArr(
x: bigint.BigInteger,
size: number,
): Uint8Array {
const byteArr = new Uint8Array(size);
const arr = x.toArray(256).value.reverse();
byteArr.set(arr, 0);
return byteArr;
}
export function bigintFromNaclArr(arr: Uint8Array): bigint.BigInteger {
let rev = new Uint8Array(arr);
rev = rev.reverse();
return bigint.fromArray(Array.from(rev), 256, false);
}
export namespace Edx25519 {
const revL = [
0xed, 0xd3, 0xf5, 0x5c, 0x1a, 0x63, 0x12, 0x58, 0xd6, 0x9c, 0xf7, 0xa2,
@ -846,9 +887,9 @@ export namespace Edx25519 {
): Promise<OpaqueData> {
const res = kdfKw({
outputLength: 64,
salt: stringToBytes("edx2559-derivation"),
salt: decodeCrock(seed),
ikm: decodeCrock(pub),
info: decodeCrock(seed),
info: stringToBytes("edx2559-derivation"),
});
return encodeCrock(res);
@ -860,28 +901,191 @@ export namespace Edx25519 {
): Promise<Edx25519PrivateKey> {
const pub = await getPublic(priv);
const privDec = decodeCrock(priv);
const privA = privDec.subarray(0, 32).reverse();
const a = bigint.fromArray(Array.from(privA), 256, false);
const a = bigintFromNaclArr(privDec.subarray(0, 32));
const factorEnc = await deriveFactor(pub, seed);
const factorModL = bigintFromNaclArr(decodeCrock(factorEnc)).mod(L);
const factorBuf = await deriveFactor(pub, seed);
const aPrime = a.divide(8).multiply(factorModL).mod(L).multiply(8).mod(L);
const bPrime = nacl
.hash(
typedArrayConcat([privDec.subarray(32, 64), decodeCrock(factorEnc)]),
)
.subarray(0, 32);
const factor = bigint.fromArray(Array.from(factorBuf), 256, false);
const aPrime = a.divide(8).multiply(factor).multiply(8);
const bPrime = nacl.hash(
typedArrayConcat([privDec.subarray(32, 64), decodeCrock(factorBuf)]),
const newPriv = encodeCrock(
typedArrayConcat([bigintToNaclArr(aPrime, 32), bPrime]),
);
Uint8Array.from(aPrime.toArray(256).value)
return newPriv;
}
export async function publicKeyDerive(
pub: Edx25519PublicKey,
seed: OpaqueData,
): Promise<Edx25519PublicKey> {
const factorEnc = await deriveFactor(pub, seed);
const factorReduced = nacl.crypto_core_ed25519_scalar_reduce(
decodeCrock(factorEnc),
);
const res = nacl.crypto_scalarmult_ed25519_noclamp(
factorReduced,
decodeCrock(pub),
);
return encodeCrock(res);
}
}
export interface AgeCommitment {
mask: number;
/**
* Public keys, one for each age group specified in the age mask.
*/
publicKeys: Edx25519PublicKey[];
}
export interface AgeProof {
/**
* Private keys. Typically smaller than the number of public keys,
* because we drop private keys from age groups that are restricted.
*/
privateKeys: Edx25519PrivateKey[];
}
export interface AgeCommitmentProof {
commitment: AgeCommitment;
proof: AgeProof;
}
function invariant(cond: boolean): asserts cond {
if (!cond) {
throw Error("invariant failed");
}
}
export namespace AgeRestriction {
export function hashCommitment(ac: AgeCommitment): HashCodeString {
const hc = new nacl.HashState();
for (const pub of ac.publicKeys) {
hc.update(decodeCrock(pub));
}
return encodeCrock(hc.finish().subarray(0, 32));
}
export function countAgeGroups(mask: number): number {
let count = 0;
let m = mask;
while (m > 0) {
count += m & 1;
m = m >> 1;
}
return count;
}
export function getAgeGroupIndex(mask: number, age: number): number {
invariant((mask & 1) === 1);
let i = 0;
let m = mask;
let a = age;
while (m > 0) {
if (a <= 0) {
break;
}
m = m >> 1;
i += m & 1;
a--;
}
return i;
}
export function ageGroupSpecToMask(ageGroupSpec: string): number {
throw Error("not implemented");
}
export function publicKeyDerive(
priv: Edx25519PrivateKey,
seed: OpaqueData,
): Promise<Edx25519PublicKey> {
throw Error("not implemented")
export async function restrictionCommit(
ageMask: number,
age: number,
): 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 priv = await Edx25519.keyCreate();
const pub = await Edx25519.getPublic(priv);
pubs.push(pub);
if (i < numPrivs) {
privs.push(priv);
}
}
return {
commitment: {
mask: ageMask,
publicKeys: pubs,
},
proof: {
privateKeys: privs,
},
};
}
export async function commitmentDerive(
commitmentProof: AgeCommitmentProof,
salt: OpaqueData,
): Promise<AgeCommitmentProof> {
const newPrivs: Edx25519PrivateKey[] = [];
const newPubs: Edx25519PublicKey[] = [];
for (const oldPub of commitmentProof.commitment.publicKeys) {
newPubs.push(await Edx25519.publicKeyDerive(oldPub, salt));
}
for (const oldPriv of commitmentProof.proof.privateKeys) {
newPrivs.push(await Edx25519.privateKeyDerive(oldPriv, salt));
}
return {
commitment: {
mask: commitmentProof.commitment.mask,
publicKeys: newPubs,
},
proof: {
privateKeys: newPrivs,
},
};
}
export function commitmentAttest(
commitmentProof: AgeCommitmentProof,
age: number,
): Edx25519Signature {
const d = buildSigPS(TalerSignaturePurpose.WALLET_AGE_ATTESTATION)
.put(bufferForUint32(commitmentProof.commitment.mask))
.put(bufferForUint32(age))
.build();
const group = getAgeGroupIndex(commitmentProof.commitment.mask, age);
if (group === 0) {
// No attestation required.
return encodeCrock(new Uint8Array(64));
}
const priv = commitmentProof.proof.privateKeys[group - 1];
const pub = commitmentProof.commitment.publicKeys[group - 1];
const sig = nacl.crypto_edx25519_sign_detached(
d,
decodeCrock(priv),
decodeCrock(pub),
);
return encodeCrock(sig);
}
export function commitmentVerify(
commitmentProof: AgeCommitmentProof,
age: number,
): Edx25519Signature {
throw Error("not implemented");
}
}

View File

@ -47,6 +47,7 @@ import {
} from "./time.js";
import { codecForAmountString } from "./amounts.js";
import { strcmp } from "./helpers.js";
import { Edx25519PublicKey } from "./talerCrypto.js";
/**
* Denomination as found in the /keys response from the exchange.
@ -283,6 +284,10 @@ export interface CoinDepositPermission {
* URL of the exchange this coin was withdrawn from.
*/
exchange_url: string;
minimum_age_sig?: EddsaSignatureString;
age_commitment?: Edx25519PublicKey[];
}
/**
@ -539,6 +544,8 @@ export interface ContractTerms {
*/
max_wire_fee?: string;
minimum_age?: number;
/**
* Extra data, interpreted by the mechant only.
*/
@ -957,6 +964,7 @@ export interface ExchangeMeltRequest {
denom_sig: UnblindedSignature;
rc: string;
value_with_fee: AmountString;
age_commitment_hash?: HashCodeString;
}
export interface ExchangeMeltResponse {
@ -1122,7 +1130,7 @@ export type DenominationPubKey = RsaDenominationPubKey | CsDenominationPubKey;
export interface RsaDenominationPubKey {
readonly cipher: DenomKeyType.Rsa;
readonly rsa_public_key: string;
readonly age_mask?: number;
readonly age_mask: number;
}
export interface CsDenominationPubKey {
@ -1177,12 +1185,14 @@ export const codecForRsaDenominationPubKey = () =>
buildCodecForObject<RsaDenominationPubKey>()
.property("cipher", codecForConstString(DenomKeyType.Rsa))
.property("rsa_public_key", codecForString())
.property("age_mask", codecForNumber())
.build("DenominationPubKey");
export const codecForCsDenominationPubKey = () =>
buildCodecForObject<CsDenominationPubKey>()
.property("cipher", codecForConstString(DenomKeyType.ClauseSchnorr))
.property("cs_public_key", codecForString())
.property("age_mask", codecForNumber())
.build("CsDenominationPubKey");
export const codecForBankWithdrawalOperationPostResponse =
@ -1312,6 +1322,7 @@ export const codecForContractTerms = (): Codec<ContractTerms> =>
.property("exchanges", codecForList(codecForExchangeHandle()))
.property("products", codecOptional(codecForList(codecForProduct())))
.property("extra", codecForAny())
.property("minimum_age", codecOptional(codecForNumber()))
.build("ContractTerms");
export const codecForMerchantRefundPermission =
@ -1717,6 +1728,13 @@ export interface ExchangeRefreshRevealRequest {
transfer_pub: EddsaPublicKeyString;
link_sigs: EddsaSignatureString[];
/**
* Iff the corresponding denomination has support for age restriction,
* the client MUST provide the original age commitment, i.e. the vector
* of public keys.
*/
old_age_commitment?: Edx25519PublicKey[];
}
export interface DepositSuccess {

View File

@ -47,6 +47,7 @@ import {
codecForConstString,
codecForAny,
buildCodecForUnion,
codecForNumber,
} from "./codec.js";
import {
AmountString,
@ -61,6 +62,7 @@ import { OrderShortInfo, codecForOrderShortInfo } from "./transactionsTypes.js";
import { BackupRecovery } from "./backupTypes.js";
import { PaytoUri } from "./payto.js";
import { TalerErrorCode } from "./taler-error-codes.js";
import { AgeCommitmentProof } from "./talerCrypto.js";
/**
* Response for the create reserve request to the wallet.
@ -218,6 +220,8 @@ export interface CreateReserveRequest {
* from this reserve, only used for testing.
*/
forcedDenomSel?: ForcedDenomSel;
restrictAge?: number;
}
export const codecForCreateReserveRequest = (): Codec<CreateReserveRequest> =>
@ -489,6 +493,7 @@ export interface WithdrawalPlanchet {
coinEv: CoinEnvelope;
coinValue: AmountJson;
coinEvHash: string;
ageCommitmentProof?: AgeCommitmentProof;
}
export interface PlanchetCreationRequest {
@ -499,6 +504,7 @@ export interface PlanchetCreationRequest {
denomPub: DenominationPubKey;
reservePub: string;
reservePriv: string;
restrictAge?: number;
}
/**
@ -545,6 +551,10 @@ export interface DepositInfo {
denomKeyType: DenomKeyType;
denomPubHash: string;
denomSig: UnblindedSignature;
requiredMinimumAge?: number;
ageCommitmentProof?: AgeCommitmentProof;
}
export interface ExchangesListRespose {
@ -728,12 +738,14 @@ export const codecForAcceptManualWithdrawalRequet =
export interface GetWithdrawalDetailsForAmountRequest {
exchangeBaseUrl: string;
amount: string;
restrictAge?: number;
}
export interface AcceptBankIntegratedWithdrawalRequest {
talerWithdrawUri: string;
exchangeBaseUrl: string;
forcedDenomSel?: ForcedDenomSel;
restrictAge?: number;
}
export const codecForAcceptBankIntegratedWithdrawalRequest =
@ -742,6 +754,7 @@ export const codecForAcceptBankIntegratedWithdrawalRequest =
.property("exchangeBaseUrl", codecForString())
.property("talerWithdrawUri", codecForString())
.property("forcedDenomSel", codecForAny())
.property("restrictAge", codecOptional(codecForNumber()))
.build("AcceptBankIntegratedWithdrawalRequest");
export const codecForGetWithdrawalDetailsForAmountRequest =
@ -774,11 +787,13 @@ export const codecForApplyRefundRequest = (): Codec<ApplyRefundRequest> =>
export interface GetWithdrawalDetailsForUriRequest {
talerWithdrawUri: string;
restrictAge?: number;
}
export const codecForGetWithdrawalDetailsForUri =
(): Codec<GetWithdrawalDetailsForUriRequest> =>
buildCodecForObject<GetWithdrawalDetailsForUriRequest>()
.property("talerWithdrawUri", codecForString())
.property("restrictAge", codecOptional(codecForNumber()))
.build("GetWithdrawalDetailsForUriRequest");
export interface ListKnownBankAccountsRequest {

View File

@ -24,6 +24,7 @@ export interface CoinCoinfigCommon {
feeDeposit: string;
feeRefresh: string;
feeRefund: string;
ageRestricted?: boolean;
}
export interface CoinConfigRsa extends CoinCoinfigCommon {

View File

@ -430,6 +430,9 @@ function setCoin(config: Configuration, c: CoinConfig) {
config.setString(s, "fee_withdraw", c.feeWithdraw);
config.setString(s, "fee_refresh", c.feeRefresh);
config.setString(s, "fee_refund", c.feeRefund);
if (c.ageRestricted) {
config.setString(s, "age_restricted", "yes");
}
if (c.cipher === "RSA") {
config.setString(s, "rsa_keysize", `${c.rsaKeySize}`);
config.setString(s, "cipher", "RSA");
@ -1112,6 +1115,17 @@ export class ExchangeService implements ExchangeServiceInterface {
config.write(this.configFilename);
}
enableAgeRestrictions(maskStr: string) {
const config = Configuration.load(this.configFilename);
config.setString("exchange-extension-age_restriction", "enabled", "yes");
config.setString(
"exchange-extension-age_restriction",
"age_groups",
maskStr,
);
config.write(this.configFilename);
}
get masterPub() {
return encodeCrock(this.keyPair.eddsaPub);
}
@ -1645,8 +1659,14 @@ export class MerchantService implements MerchantServiceInterface {
await exec(`taler-merchant-dbinit -c "${this.configFilename}"`);
this.proc = this.globalState.spawnService(
"valgrind",
[
"taler-merchant-httpd",
["-LDEBUG", "-c", this.configFilename, ...this.timetravelArgArr],
"-LDEBUG",
"-c",
this.configFilename,
...this.timetravelArgArr,
],
`merchant-${this.merchantConfig.name}`,
);
}
@ -1848,6 +1868,9 @@ export async function runTestWithState(
}
} catch (e) {
console.error("FATAL: test failed with exception", e);
if (e instanceof TalerError) {
console.error(`error detail: ${j2s(e.errorDetail)}`);
}
status = "fail";
} finally {
await gc.shutdown();

View File

@ -65,6 +65,13 @@ export interface SimpleTestEnvironment {
wallet: WalletCli;
}
export interface EnvOptions {
/**
* If provided, enable age restrictions with the specified age mask string.
*/
ageMaskSpec?: string;
}
/**
* Run a test case with a simple TESTKUDOS Taler environment, consisting
* of one exchange, one bank and one merchant.
@ -72,6 +79,7 @@ export interface SimpleTestEnvironment {
export async function createSimpleTestkudosEnvironment(
t: GlobalTestState,
coinConfig: CoinConfig[] = defaultCoinConfig.map((x) => x("TESTKUDOS")),
opts: EnvOptions = {},
): Promise<SimpleTestEnvironment> {
const db = await setupDb(t);
@ -108,7 +116,17 @@ export async function createSimpleTestkudosEnvironment(
await bank.pingUntilAvailable();
const ageMaskSpec = opts.ageMaskSpec;
if (ageMaskSpec) {
exchange.enableAgeRestrictions(ageMaskSpec);
// Enable age restriction for all coins.
exchange.addCoinConfigList(
coinConfig.map((x) => ({ ...x, ageRestricted: true })),
);
} else {
exchange.addCoinConfigList(coinConfig);
}
await exchange.start();
await exchange.pingUntilAvailable();
@ -259,6 +277,7 @@ export async function startWithdrawViaBank(
bank: BankService;
exchange: ExchangeServiceInterface;
amount: AmountString;
restrictAge?: number;
},
): Promise<void> {
const { wallet, bank, exchange, amount } = p;
@ -270,6 +289,7 @@ export async function startWithdrawViaBank(
await wallet.client.call(WalletApiOperation.GetWithdrawalDetailsForUri, {
talerWithdrawUri: wop.taler_withdraw_uri,
restrictAge: p.restrictAge,
});
await wallet.runPending();
@ -279,6 +299,7 @@ export async function startWithdrawViaBank(
await wallet.client.call(WalletApiOperation.AcceptBankIntegratedWithdrawal, {
exchangeBaseUrl: exchange.baseUrl,
talerWithdrawUri: wop.taler_withdraw_uri,
restrictAge: p.restrictAge,
});
// Confirm it
@ -299,6 +320,7 @@ export async function withdrawViaBank(
bank: BankService;
exchange: ExchangeServiceInterface;
amount: AmountString;
restrictAge?: number;
},
): Promise<void> {
const { wallet } = p;

View File

@ -0,0 +1,64 @@
/*
This file is part of GNU Taler
(C) 2022 Taler Systems S.A.
GNU Taler is free software; you can redistribute it and/or modify it under the
terms of the GNU General Public License as published by the Free Software
Foundation; either version 3, or (at your option) any later version.
GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
/**
* Imports.
*/
import { defaultCoinConfig } from "../harness/denomStructures.js";
import { GlobalTestState } from "../harness/harness.js";
import {
createSimpleTestkudosEnvironment,
withdrawViaBank,
makeTestPayment,
} from "../harness/helpers.js";
/**
* Run test for basic, bank-integrated withdrawal and payment.
*/
export async function runAgeRestrictionsTest(t: GlobalTestState) {
// Set up test environment
const { wallet, bank, exchange, merchant } =
await createSimpleTestkudosEnvironment(
t,
defaultCoinConfig.map((x) => x("TESTKUDOS")),
{
ageMaskSpec: "8:10:12:14:16:18:21",
},
);
// Withdraw digital cash into the wallet.
await withdrawViaBank(t, {
wallet,
bank,
exchange,
amount: "TESTKUDOS:20",
restrictAge: 13,
});
const order = {
summary: "Buy me!",
amount: "TESTKUDOS:5",
fulfillment_url: "taler://fulfillment-success/thx",
minimum_age: 9,
};
await makeTestPayment(t, { wallet, merchant, order });
await wallet.runUntilDone();
}
runAgeRestrictionsTest.suites = ["wallet"];

View File

@ -25,6 +25,7 @@ import {
shouldLingerInTest,
TestRunResult,
} from "../harness/harness.js";
import { runAgeRestrictionsTest } from "./test-age-restrictions.js";
import { runBankApiTest } from "./test-bank-api";
import { runClaimLoopTest } from "./test-claim-loop";
import { runClauseSchnorrTest } from "./test-clause-schnorr.js";
@ -103,6 +104,7 @@ interface TestMainFunction {
}
const allTests: TestMainFunction[] = [
runAgeRestrictionsTest,
runBankApiTest,
runClaimLoopTest,
runClauseSchnorrTest,

View File

@ -69,6 +69,10 @@ import {
kdf,
ecdheGetPublic,
getRandomBytes,
AgeCommitmentProof,
AgeRestriction,
hashCoinPub,
HashCodeString,
} from "@gnu-taler/taler-util";
import bigint from "big-integer";
import { DenominationRecord, TipCoinSource, WireFee } from "../db.js";
@ -82,7 +86,7 @@ import {
SignTrackTransactionRequest,
} from "./cryptoTypes.js";
//const logger = new Logger("cryptoImplementation.ts");
const logger = new Logger("cryptoImplementation.ts");
/**
* Interface for (asynchronous) cryptographic operations that
@ -547,12 +551,34 @@ export const nativeCryptoR: TalerCryptoInterfaceR = {
const denomPub = req.denomPub;
if (denomPub.cipher === DenomKeyType.Rsa) {
const reservePub = decodeCrock(req.reservePub);
const denomPubRsa = decodeCrock(denomPub.rsa_public_key);
const derivedPlanchet = await tci.setupWithdrawalPlanchet(tci, {
coinNumber: req.coinIndex,
secretSeed: req.secretSeed,
});
const coinPubHash = hash(decodeCrock(derivedPlanchet.coinPub));
let maybeAcp: AgeCommitmentProof | undefined = undefined;
let maybeAgeCommitmentHash: string | undefined = undefined;
if (req.restrictAge) {
if (denomPub.age_mask === 0) {
throw Error(
"requested age restriction for a denomination that does not support age restriction",
);
}
logger.info("creating age-restricted planchet");
maybeAcp = await AgeRestriction.restrictionCommit(
denomPub.age_mask,
req.restrictAge,
);
maybeAgeCommitmentHash = AgeRestriction.hashCommitment(
maybeAcp.commitment,
);
}
const coinPubHash = hashCoinPub(
derivedPlanchet.coinPub,
maybeAgeCommitmentHash,
);
const blindResp = await tci.rsaBlind(tci, {
bks: derivedPlanchet.bks,
hm: encodeCrock(coinPubHash),
@ -589,6 +615,7 @@ export const nativeCryptoR: TalerCryptoInterfaceR = {
reservePub: encodeCrock(reservePub),
withdrawSig: sigResult.sig,
coinEvHash: encodeCrock(evHash),
ageCommitmentProof: maybeAcp,
};
return planchet;
} else {
@ -880,7 +907,23 @@ export const nativeCryptoR: TalerCryptoInterfaceR = {
): Promise<CoinDepositPermission> {
// FIXME: put extensions here if used
const hExt = new Uint8Array(64);
const hAgeCommitment = new Uint8Array(32);
let hAgeCommitment: Uint8Array;
let maybeAgeCommitmentHash: string | undefined = undefined;
let minimumAgeSig: string | undefined = undefined;
if (depositInfo.ageCommitmentProof) {
const ach = AgeRestriction.hashCommitment(
depositInfo.ageCommitmentProof.commitment,
);
maybeAgeCommitmentHash = ach;
hAgeCommitment = decodeCrock(ach);
minimumAgeSig = AgeRestriction.commitmentAttest(
depositInfo.ageCommitmentProof,
depositInfo.requiredMinimumAge!,
);
} else {
// All zeros.
hAgeCommitment = new Uint8Array(32);
}
let d: Uint8Array;
if (depositInfo.denomKeyType === DenomKeyType.Rsa) {
d = buildSigPS(TalerSignaturePurpose.WALLET_COIN_DEPOSIT)
@ -914,6 +957,8 @@ export const nativeCryptoR: TalerCryptoInterfaceR = {
cipher: DenomKeyType.Rsa,
rsa_signature: depositInfo.denomSig.rsa_signature,
},
age_commitment: depositInfo.ageCommitmentProof?.commitment.publicKeys,
minimum_age_sig: minimumAgeSig,
};
return s;
} else {
@ -999,10 +1044,19 @@ export const nativeCryptoR: TalerCryptoInterfaceR = {
coinNumber: coinIndex,
transferSecret: transferSecretRes.h,
});
let newAc: AgeCommitmentProof | undefined = undefined;
let newAch: HashCodeString | undefined = undefined;
if (req.meltCoinAgeCommitmentProof) {
newAc = await AgeRestriction.commitmentDerive(
req.meltCoinAgeCommitmentProof,
transferSecretRes.h,
);
newAch = AgeRestriction.hashCommitment(newAc.commitment);
}
coinPriv = decodeCrock(fresh.coinPriv);
coinPub = decodeCrock(fresh.coinPub);
blindingFactor = decodeCrock(fresh.bks);
const coinPubHash = hash(coinPub);
const coinPubHash = hashCoinPub(fresh.coinPub, newAch);
if (denomSel.denomPub.cipher !== DenomKeyType.Rsa) {
throw Error("unsupported cipher, can't create refresh session");
}
@ -1035,8 +1089,16 @@ export const nativeCryptoR: TalerCryptoInterfaceR = {
const sessionHash = sessionHc.finish();
let confirmData: Uint8Array;
// FIXME: fill in age commitment
const hAgeCommitment = new Uint8Array(32);
let hAgeCommitment: Uint8Array;
if (req.meltCoinAgeCommitmentProof) {
hAgeCommitment = decodeCrock(
AgeRestriction.hashCommitment(
req.meltCoinAgeCommitmentProof.commitment,
),
);
} else {
hAgeCommitment = new Uint8Array(32);
}
confirmData = buildSigPS(TalerSignaturePurpose.WALLET_COIN_MELT)
.put(sessionHash)
.put(decodeCrock(meltCoinDenomPubHash))

View File

@ -28,6 +28,7 @@
* Imports.
*/
import {
AgeCommitmentProof,
AmountJson,
CoinEnvelope,
DenominationPubKey,
@ -55,6 +56,7 @@ export interface DeriveRefreshSessionRequest {
meltCoinPub: string;
meltCoinPriv: string;
meltCoinDenomPubHash: string;
meltCoinAgeCommitmentProof?: AgeCommitmentProof;
newCoinDenoms: RefreshNewDenomInfo[];
feeRefresh: AmountJson;
}

View File

@ -321,9 +321,9 @@ export class CryptoDispatcher {
return new Promise<T>((resolve, reject) => {
let timedOut = false;
const timeout = timer.after(5000, () => {
logger.warn("crypto RPC call timed out");
logger.warn(`crypto RPC call ('${operation}') timed out`);
timedOut = true;
reject(new Error("crypto RPC call timed out"));
reject(new Error(`crypto RPC call ('${operation}') timed out`));
});
p.then((x) => {
if (timedOut) {

View File

@ -40,6 +40,7 @@ import {
CoinEnvelope,
TalerProtocolTimestamp,
TalerProtocolDuration,
AgeCommitmentProof,
} from "@gnu-taler/taler-util";
import { RetryInfo } from "./util/retries.js";
import { PayCoinSelection } from "./util/coinSelection.js";
@ -188,6 +189,15 @@ export interface ReserveRecord {
*/
bankInfo?: ReserveBankInfo;
/**
* Restrict withdrawals from this reserve to this age.
*/
restrictAge?: number;
/**
* Pre-allocated ID of the withdrawal group for the first withdrawal
* on this reserve.
*/
initialWithdrawalGroupId: string;
/**
@ -600,6 +610,8 @@ export interface PlanchetRecord {
coinEv: CoinEnvelope;
coinEvHash: string;
ageCommitmentProof?: AgeCommitmentProof;
}
/**
@ -724,6 +736,8 @@ export interface CoinRecord {
* Used to prevent allocation of the same coin for two different payments.
*/
allocation?: CoinAllocation;
ageCommitmentProof?: AgeCommitmentProof;
}
export interface CoinAllocation {
@ -1148,6 +1162,7 @@ export interface WalletContractData {
wireMethod: string;
wireInfoHash: string;
maxDepositFee: AmountJson;
minimumAge?: number;
}
export enum AbortStatus {

View File

@ -33,6 +33,7 @@ import {
ExchangeSignKeyJson,
ExchangeWireJson,
hashDenomPub,
j2s,
LibtoolVersion,
Logger,
NotificationType,
@ -445,6 +446,7 @@ async function downloadExchangeKeysInfo(
);
logger.info("received /keys response");
logger.info(`${j2s(exchangeKeysJsonUnchecked)}`);
if (exchangeKeysJsonUnchecked.denoms.length === 0) {
throw TalerError.fromDetail(

View File

@ -26,6 +26,7 @@
*/
import {
AbsoluteTime,
AgeRestriction,
AmountJson,
Amounts,
codecForContractTerms,
@ -197,6 +198,14 @@ export interface CoinSelectionRequest {
maxWireFee: AmountJson;
maxDepositFee: AmountJson;
/**
* Minimum age requirement for the coin selection.
*
* When present, only select coins with either no age restriction
* or coins with an age commitment that matches the minimum age.
*/
minimumAge?: number;
}
/**
@ -651,6 +660,7 @@ export function extractContractData(
merchant: parsedContractTerms.merchant,
products: parsedContractTerms.products,
summaryI18n: parsedContractTerms.summary_i18n,
minimumAge: parsedContractTerms.minimum_age,
};
}
@ -825,6 +835,8 @@ async function processDownloadProposalImpl(
proposalResp.sig,
);
logger.trace(`extracted contract data: ${j2s(contractData)}`);
await ws.db
.mktx((x) => ({ proposals: x.proposals, purchases: x.purchases }))
.runReadWrite(async (tx) => {
@ -1379,6 +1391,11 @@ export async function generateDepositPermissions(
const { coin, denom } = coinWithDenom[i];
let wireInfoHash: string;
wireInfoHash = contractData.wireInfoHash;
logger.trace(
`signing deposit permission for coin with acp=${j2s(
coin.ageCommitmentProof,
)}`,
);
const dp = await ws.cryptoApi.signDepositPermission({
coinPriv: coin.coinPriv,
coinPub: coin.coinPub,
@ -1393,6 +1410,8 @@ export async function generateDepositPermissions(
spendAmount: payCoinSel.coinContributions[i],
timestamp: contractData.timestamp,
wireInfoHash,
ageCommitmentProof: coin.ageCommitmentProof,
requiredMinimumAge: contractData.minimumAge,
});
depositPermissions.push(dp);
}

View File

@ -15,6 +15,8 @@
*/
import {
AgeCommitment,
AgeRestriction,
CoinPublicKeyString,
DenomKeyType,
encodeCrock,
@ -22,7 +24,9 @@ import {
ExchangeProtocolVersion,
ExchangeRefreshRevealRequest,
getRandomBytes,
HashCodeString,
HttpStatusCode,
j2s,
TalerProtocolTimestamp,
} from "@gnu-taler/taler-util";
import {
@ -83,6 +87,7 @@ import { GetReadWriteAccess } from "../util/query.js";
import { guardOperationException } from "./common.js";
import { CryptoApiStoppedError } from "../crypto/workers/cryptoDispatcher.js";
import { TalerCryptoInterface } from "../crypto/cryptoImplementation.js";
import { TalerError } from "../errors.js";
const logger = new Logger("refresh.ts");
@ -380,6 +385,7 @@ async function refreshMelt(
meltCoinPriv: oldCoin.coinPriv,
meltCoinPub: oldCoin.coinPub,
feeRefresh: oldDenom.feeRefresh,
meltCoinAgeCommitmentProof: oldCoin.ageCommitmentProof,
newCoinDenoms,
sessionSecretSeed: refreshSession.sessionSecretSeed,
});
@ -388,6 +394,14 @@ async function refreshMelt(
`coins/${oldCoin.coinPub}/melt`,
oldCoin.exchangeBaseUrl,
);
let maybeAch: HashCodeString | undefined;
if (oldCoin.ageCommitmentProof) {
maybeAch = AgeRestriction.hashCommitment(
oldCoin.ageCommitmentProof.commitment,
);
}
const meltReqBody: ExchangeMeltRequest = {
coin_pub: oldCoin.coinPub,
confirm_sig: derived.confirmSig,
@ -395,6 +409,7 @@ async function refreshMelt(
denom_sig: oldCoin.denomSig,
rc: derived.hash,
value_with_fee: Amounts.stringify(derived.meltValueWithFee),
age_commitment_hash: maybeAch,
};
const resp = await ws.runSequentialized([EXCHANGE_COINS_LOCK], async () => {
@ -475,6 +490,7 @@ export async function assembleRefreshRevealRequest(args: {
denomPubHash: string;
count: number;
}[];
oldAgeCommitment?: AgeCommitment;
}): Promise<ExchangeRefreshRevealRequest> {
const {
derived,
@ -517,6 +533,7 @@ export async function assembleRefreshRevealRequest(args: {
transfer_privs: privs,
transfer_pub: derived.transferPubs[norevealIndex],
link_sigs: linkSigs,
old_age_commitment: args.oldAgeCommitment?.publicKeys,
};
return req;
}
@ -622,6 +639,7 @@ async function refreshReveal(
meltCoinPub: oldCoin.coinPub,
feeRefresh: oldDenom.feeRefresh,
newCoinDenoms,
meltCoinAgeCommitmentProof: oldCoin.ageCommitmentProof,
sessionSecretSeed: refreshSession.sessionSecretSeed,
});
@ -637,6 +655,7 @@ async function refreshReveal(
norevealIndex: norevealIndex,
oldCoinPriv: oldCoin.coinPriv,
oldCoinPub: oldCoin.coinPub,
oldAgeCommitment: oldCoin.ageCommitmentProof?.commitment,
});
const resp = await ws.runSequentialized([EXCHANGE_COINS_LOCK], async () => {
@ -822,6 +841,11 @@ async function processRefreshGroupImpl(
logger.info(
"crypto API stopped while processing refresh group, probably the wallet is currently shutting down.",
);
} else if (x instanceof TalerError) {
logger.warn("process refresh session got exception (TalerError)");
logger.warn(`exc ${x}`);
logger.warn(`exc stack ${x.stack}`);
logger.warn(`error detail: ${j2s(x.errorDetail)}`);
} else {
logger.warn("process refresh session got exception");
logger.warn(`exc ${x}`);

View File

@ -200,6 +200,7 @@ export async function createReserve(
lastError: undefined,
currency: req.amount.currency,
operationStatus: OperationStatus.Pending,
restrictAge: req.restrictAge,
};
const exchangeInfo = await updateExchangeFromUrl(ws, req.exchange);
@ -541,12 +542,9 @@ async function updateReserve(
const reserveUrl = new URL(`reserves/${reservePub}`, reserve.exchangeBaseUrl);
reserveUrl.searchParams.set("timeout_ms", "200");
const resp = await ws.http.get(
reserveUrl.href,
{
const resp = await ws.http.get(reserveUrl.href, {
timeout: getReserveRequestTimeout(reserve),
},
);
});
const result = await readSuccessResponseJsonOrErrorCode(
resp,
@ -632,17 +630,12 @@ async function updateReserve(
amountReservePlus,
amountReserveMinus,
).amount;
const denomSel = selectWithdrawalDenominations(
remainingAmount,
denoms,
);
const denomSel = selectWithdrawalDenominations(remainingAmount, denoms);
logger.trace(
`Remaining unclaimed amount in reseve is ${Amounts.stringify(
remainingAmount,
)} and can be withdrawn with ${
denomSel.selectedDenoms.length
} coins`,
)} and can be withdrawn with ${denomSel.selectedDenoms.length} coins`,
);
if (denomSel.selectedDenoms.length === 0) {
@ -759,6 +752,7 @@ export async function createTalerWithdrawReserve(
selectedExchange: string,
options: {
forcedDenomSel?: ForcedDenomSel;
restrictAge?: number;
} = {},
): Promise<AcceptWithdrawalResponse> {
await updateExchangeFromUrl(ws, selectedExchange);
@ -774,6 +768,7 @@ export async function createTalerWithdrawReserve(
exchange: selectedExchange,
senderWire: withdrawInfo.senderWire,
exchangePaytoUri: exchangePaytoUri,
restrictAge: options.restrictAge,
});
// We do this here, as the reserve should be registered before we return,
// so that we can redirect the user to the bank's status page.

View File

@ -32,6 +32,7 @@ test("withdrawal selection bug repro", (t) => {
cipher: DenomKeyType.Rsa,
rsa_public_key:
"040000XT67C8KBD6B75TTQ3SK8FWXMNQW4372T3BDDGPAMB9RFCA03638W8T3F71WFEFK9NP32VKYVNFXPYRWQ1N1HDKV5J0DFEKHBPJCYSWCBJDRNWD7G8BN8PT97FA9AMV75MYEK4X54D1HGJ207JSVJBGFCATSPNTEYNHEQF1F220W00TBZR1HNPDQFD56FG0DJQ9KGHM8EC33H6AY9YN9CNX5R3Z4TZ4Q23W47SBHB13H6W74FQJG1F50X38VRSC4SR8RWBAFB7S4K8D2H4NMRFSQT892A3T0BTBW7HM5C0H2CK6FRKG31F7W9WP1S29013K5CXYE55CT8TH6N8J9B780R42Y5S3ZB6J6E9H76XBPSGH4TGYSR2VZRB98J417KCQMZKX1BB67E7W5KVE37TC9SJ904002",
age_mask: 0,
},
denomPubHash:
"Q21FQSSG4FXNT96Z14CHXM8N1RZAG9GPHAV8PRWS0PZAAVWH7PBW6R97M2CH19KKP65NNSWXY7B6S53PT3CBM342E357ZXDDJ8RDVW8",
@ -86,6 +87,7 @@ test("withdrawal selection bug repro", (t) => {
cipher: DenomKeyType.Rsa,
rsa_public_key:
"040000Y63CF78QFPKRY77BRK9P557Q1GQWX3NCZ3HSYSK0Z7TT0KGRA7N4SKBKEHSTVHX1Z9DNXMJR4EXSY1TXCKV0GJ3T3YYC6Z0JNMJFVYQAV4FX5J90NZH1N33MZTV8HS9SMNAA9S6K73G4P99GYBB01B0P6M1KXZ5JRDR7VWBR3MEJHHGJ6QBMCJR3NWJRE3WJW9PRY8QPQ2S7KFWTWRESH2DBXCXWBD2SRN6P9YX8GRAEMFEGXC9V5GVJTEMH6ZDGNXFPWZE3JVJ2Q4N9GDYKBCHZCJ7M7M2RJ9ZV4Y64NAN9BT6XDC68215GKKRHTW1BBF1MYY6AR3JCTT9HYAM923RMVQR3TAEB7SDX8J76XRZWYH3AGJCZAQGMN5C8SSH9AHQ9RNQJQ15CN45R37X4YNFJV904002",
age_mask: 0,
},
denomPubHash:
@ -141,6 +143,7 @@ test("withdrawal selection bug repro", (t) => {
cipher: DenomKeyType.Rsa,
rsa_public_key:
"040000YDESWC2B962DA4WK356SC50MA3N9KV0ZSGY3RC48JCTY258W909C7EEMT5BTC5KZ5T4CERCZ141P9QF87EK2BD1XEEM5GB07MB3H19WE4CQGAS8X84JBWN83PQGQXVMWE5HFA992KMGHC566GT9ZS2QPHZB6X89C4A80Z663PYAAPXP728VHAKATGNNBQ01ZZ2XD1CH9Y38YZBSPJ4K7GB2J76GBCYAVD9ENHDVWXJAXYRPBX4KSS5TXRR3K5NEN9ZV3AJD2V65K7ABRZDF5D5V1FJZZMNJ5XZ4FEREEKEBV9TDFPGJTKDEHEC60K3DN24DAATRESDJ1ZYYSYSRCAT4BT2B62ARGVMJTT5N2R126DRW9TGRWCW0ZAF2N2WET1H4NJEW77X0QT46Z5R3MZ0XPHD04002",
age_mask: 0,
},
denomPubHash:
"JS61DTKAFM0BX8Q4XV3ZSKB921SM8QK745Z2AFXTKFMBHHFNBD8TQ5ETJHFNDGBGX22FFN2A2ERNYG1SGSDQWNQHQQ2B14DBVJYJG8R",
@ -195,6 +198,7 @@ test("withdrawal selection bug repro", (t) => {
cipher: DenomKeyType.Rsa,
rsa_public_key:
"040000YG3T1ADB8DVA6BD3EPV6ZHSHTDW35DEN4VH1AE6CSB7P1PSDTNTJG866PHF6QB1CCWYCVRGA0FVBJ9Q0G7KV7AD9010GDYBQH0NNPHW744MTNXVXWBGGGRGQGYK4DTYN1DSWQ1FZNDSZZPB5BEKG2PDJ93NX2JTN06Y8QMS2G734Z9XHC10EENBG2KVB7EJ3CM8PV1T32RC7AY62F3496E8D8KRHJQQTT67DSGMNKK86QXVDTYW677FG27DP20E8XY3M6FQD53NDJ1WWES91401MV1A3VXVPGC76GZVDD62W3WTJ1YMKHTTA3MRXX3VEAAH3XTKDN1ER7X6CZPMYTF8VK735VP2B2TZGTF28TTW4FZS32SBS64APCDF6SZQ427N5538TJC7SRE71YSP5ET8GS904002",
age_mask: 0,
},
denomPubHash:
@ -250,6 +254,7 @@ test("withdrawal selection bug repro", (t) => {
cipher: DenomKeyType.Rsa,
rsa_public_key:
"040000ZC0G60E9QQ5PD81TSDWD9GV5Y6P8Z05NSPA696DP07NGQQVSRQXBA76Q6PRB0YFX295RG4MTQJXAZZ860ET307HSC2X37XAVGQXRVB8Q4F1V7NP5ZEVKTX75DZK1QRAVHEZGQYKSSH6DBCJNQF6V9WNQF3GEYVA4KCBHA7JF772KHXM9642C28Z0AS4XXXV2PABAN5C8CHYD5H7JDFNK3920W5Q69X0BS84XZ4RE2PW6HM1WZ6KGZ3MKWWWCPKQ1FSFABRBWKAB09PF563BEBXKY6M38QETPH5EDWGANHD0SC3QV0WXYVB7BNHNNQ0J5BNV56K563SYHM4E5ND260YRJSYA1GN5YSW2B1J5T1A1EBNYF2DN6JNJKWXWEQ42G5YS17ZSZ5EWDRA9QKV8EGTCNAD04002",
age_mask: 0,
},
denomPubHash:
"A41HW0Q2H9PCNMEWW0C0N45QAYVXZ8SBVRRAHE4W6X24SV1TH38ANTWDT80JXEBW9Z8PVPGT9GFV2EYZWJ5JW5W1N34NFNKHQSZ1PFR",
@ -304,6 +309,7 @@ test("withdrawal selection bug repro", (t) => {
cipher: DenomKeyType.Rsa,
rsa_public_key:
"040000ZSK2PMVY6E3NBQ52KXMW029M60F4BWYTDS0FZSD0PE53CNZ9H6TM3GQK1WRTEKQ5GRWJ1J9DY6Y42SP47QVT1XD1G0W05SQ5F3F7P5KSWR0FJBJ9NZBXQEVN8Q4JRC94X3JJ3XV3KBYTZ2HTDFV28C3H2SRR0XGNZB4FY85NDZF1G4AEYJJ9QB3C0V8H70YB8RV3FKTNH7XS4K4HFNZHJ5H9VMX5SM9Z2DX37HA5WFH0E2MJBVVF2BWWA5M0HPPSB365RAE2AMD42Q65A96WD80X27SB2ZNQZ8WX0K13FWF85GZ6YNYAJGE1KGN06JDEKE9QD68Z651D7XE8V6664TVVC8M68S7WD0DSXMJQKQ0BNJXNDE29Q7MRX6DA3RW0PZ44B3TKRK0294FPVZTNSTA6XF04002",
age_mask: 0,
},
denomPubHash:
"F5NGBX33DTV4595XZZVK0S2MA1VMXFEJQERE5EBP5DS4QQ9EFRANN7YHWC1TKSHT2K6CQWDBRES8D3DWR0KZF5RET40B4AZXZ0RW1ZG",

View File

@ -266,8 +266,6 @@ export function selectForcedWithdrawalDenominations(
denoms: DenominationRecord[],
forcedDenomSel: ForcedDenomSel,
): DenomSelectionState {
let remaining = Amounts.copy(amountAvailable);
const selectedDenoms: {
count: number;
denomPubHash: string;
@ -454,6 +452,7 @@ async function processPlanchetGenerate(
value: denom.value,
coinIndex: coinIdx,
secretSeed: withdrawalGroup.secretSeed,
restrictAge: reserve.restrictAge,
});
const newPlanchet: PlanchetRecord = {
blindingKey: r.blindingKey,
@ -467,6 +466,7 @@ async function processPlanchetGenerate(
withdrawalDone: false,
withdrawSig: r.withdrawSig,
withdrawalGroupId: withdrawalGroup.withdrawalGroupId,
ageCommitmentProof: r.ageCommitmentProof,
lastError: undefined,
};
await ws.db
@ -701,6 +701,7 @@ async function processPlanchetVerifyAndStoreCoin(
withdrawalGroupId: withdrawalGroup.withdrawalGroupId,
},
suspended: false,
ageCommitmentProof: planchet.ageCommitmentProof,
};
const planchetCoinPub = planchet.coinPub;
@ -1101,11 +1102,6 @@ export async function getExchangeWithdrawalInfo(
}
}
const withdrawFee = Amounts.sub(
selectedDenoms.totalWithdrawCost,
selectedDenoms.totalCoinValue,
).amount;
const ret: ExchangeWithdrawDetails = {
earliestDepositExpiration,
exchangeInfo: exchange,
@ -1127,6 +1123,10 @@ export async function getExchangeWithdrawalInfo(
return ret;
}
export interface GetWithdrawalDetailsForUriOpts {
restrictAge?: number;
}
/**
* Get more information about a taler://withdraw URI.
*
@ -1137,6 +1137,7 @@ export async function getExchangeWithdrawalInfo(
export async function getWithdrawalDetailsForUri(
ws: InternalWalletState,
talerWithdrawUri: string,
opts: GetWithdrawalDetailsForUriOpts = {},
): Promise<WithdrawUriInfoResponse> {
logger.trace(`getting withdrawal details for URI ${talerWithdrawUri}`);
const info = await getBankWithdrawalInfo(ws.http, talerWithdrawUri);

View File

@ -36,6 +36,7 @@ function fakeAci(current: string, feeDeposit: string): AvailableCoinInfo {
denomPub: {
cipher: DenomKeyType.Rsa,
rsa_public_key: "foobar",
age_mask: 0,
},
feeDeposit: a(feeDeposit),
exchangeBaseUrl: "https://example.com/",

View File

@ -843,6 +843,7 @@ async function dispatchRequestInternal(
req.exchangeBaseUrl,
{
forcedDenomSel: req.forcedDenomSel,
restrictAge: req.restrictAge,
},
);
}
@ -1207,7 +1208,7 @@ class InternalWalletStateImpl implements InternalWalletState {
) {
this.cryptoDispatcher = new CryptoDispatcher(cryptoWorkerFactory);
this.cryptoApi = this.cryptoDispatcher.cryptoApi;
this.timerGroup = new TimerGroup(timer)
this.timerGroup = new TimerGroup(timer);
}
async getDenomInfo(