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); return crypto_scalarmult(q, n, _9);
} }
function crypto_scalarmult_noclamp( export function crypto_scalarmult_noclamp(
q: Uint8Array, q: Uint8Array,
n: Uint8Array, n: Uint8Array,
p: Uint8Array, p: Uint8Array,
@ -3033,6 +3033,18 @@ export function crypto_core_ed25519_scalar_add(
return o; 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( export function crypto_core_ed25519_scalar_sub(
x: Uint8Array, x: Uint8Array,
y: 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 { export function crypto_edx25519_get_public(priv: Uint8Array): Uint8Array {
const pub = new Uint8Array(32); return crypto_scalarmult_ed25519_base_noclamp(priv.subarray(0, 32));
if (0 != crypto_scalarmult_base_noclamp(pub.subarray(32), priv)) {
throw Error();
}
return pub;
} }
export function crypto_edx25519_sign_detached( export function crypto_edx25519_sign_detached(
@ -3076,19 +3084,16 @@ export function crypto_edx25519_sign_detached(
pkx: Uint8Array, pkx: Uint8Array,
): Uint8Array { ): Uint8Array {
const n: number = m.length; const n: number = m.length;
const d = new Uint8Array(64), const h = new Uint8Array(64);
h = new Uint8Array(64), const r = new Uint8Array(64);
r = new Uint8Array(64);
let i, j; let i, j;
const x = new Float64Array(64); const x = new Float64Array(64);
const p = [gf(), gf(), gf(), gf()]; const p = [gf(), gf(), gf(), gf()];
for (i = 0; i < 64; i++) d[i] = skx[i];
const sm = new Uint8Array(n + 64); const sm = new Uint8Array(n + 64);
for (i = 0; i < n; i++) sm[64 + i] = m[i]; 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); crypto_hash(r, sm.subarray(32), n + 32);
reduce(r); 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++) x[i] = r[i];
for (i = 0; i < 32; i++) { for (i = 0; i < 32; i++) {
for (j = 0; j < 32; j++) { 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); modL(sm.subarray(32), x);
return sm.subarray(64); return sm.subarray(0, 64);
} }
export function crypto_edx25519_sign_detached_verify( export function crypto_edx25519_sign_detached_verify(

View File

@ -34,6 +34,10 @@ import {
scalarMultBase25519, scalarMultBase25519,
deriveSecrets, deriveSecrets,
calcRBlind, calcRBlind,
Edx25519,
getRandomBytes,
bigintToNaclArr,
bigintFromNaclArr,
} from "./talerCrypto.js"; } from "./talerCrypto.js";
import { sha512, kdf } from "./kdf.js"; import { sha512, kdf } from "./kdf.js";
import * as nacl from "./nacl-fast.js"; import * as nacl from "./nacl-fast.js";
@ -44,6 +48,7 @@ import { initNodePrng } from "./prng-node.js";
initNodePrng(); initNodePrng();
import bigint from "big-integer"; import bigint from "big-integer";
import { AssertionError } from "assert"; import { AssertionError } from "assert";
import BigInteger from "big-integer";
test("encoding", (t) => { test("encoding", (t) => {
const s = "Hello, World"; const s = "Hello, World";
@ -343,9 +348,86 @@ test("taler CS blind c", async (t) => {
}; };
const sig = await csUnblind(bseed, rPub, pub, b, blindsig); const sig = await csUnblind(bseed, rPub, pub, b, blindsig);
t.deepEqual(sig.s, decodeCrock("F4ZKMFW3Q7DFN0N94KAMG2JFFHAC362T0QZ6ZCVZ73RS8P91CR70")); t.deepEqual(
t.deepEqual(sig.rPub, decodeCrock("CHK7JC4SXZ4Y9RDA3881S82F7BP99H35Q361WR6RBXN5YN2ZM1M0")); sig.s,
decodeCrock("F4ZKMFW3Q7DFN0N94KAMG2JFFHAC362T0QZ6ZCVZ73RS8P91CR70"),
);
t.deepEqual(
sig.rPub,
decodeCrock("CHK7JC4SXZ4Y9RDA3881S82F7BP99H35Q361WR6RBXN5YN2ZM1M0"),
);
const res = await csVerify(decodeCrock(msg_hash), sig, pub); const res = await csVerify(decodeCrock(msg_hash), sig, pub);
t.deepEqual(res, true); 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 { import {
Base32String, Base32String,
CoinEnvelope, CoinEnvelope,
CoinPublicKeyString,
DenominationPubKey, DenominationPubKey,
DenomKeyType, DenomKeyType,
HashCodeString, 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. * Hash a denomination public key.
*/ */
@ -652,6 +664,7 @@ export function hashDenomPub(pub: DenominationPubKey): Uint8Array {
const hashInputBuf = new ArrayBuffer(pubBuf.length + 4 + 4); const hashInputBuf = new ArrayBuffer(pubBuf.length + 4 + 4);
const uint8ArrayBuf = new Uint8Array(hashInputBuf); const uint8ArrayBuf = new Uint8Array(hashInputBuf);
const dv = new DataView(hashInputBuf); const dv = new DataView(hashInputBuf);
logger.info("age_mask", pub.age_mask);
dv.setUint32(0, pub.age_mask ?? 0); dv.setUint32(0, pub.age_mask ?? 0);
dv.setUint32(4, DenomKeyType.toIntTag(pub.cipher)); dv.setUint32(4, DenomKeyType.toIntTag(pub.cipher));
uint8ArrayBuf.set(pubBuf, 8); uint8ArrayBuf.set(pubBuf, 8);
@ -705,6 +718,14 @@ export function bufferForUint32(n: number): Uint8Array {
return buf; 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( export function setupTipPlanchet(
secretSeed: Uint8Array, secretSeed: Uint8Array,
coinNumber: number, coinNumber: number,
@ -753,6 +774,7 @@ export enum TalerSignaturePurpose {
WALLET_COIN_RECOUP = 1203, WALLET_COIN_RECOUP = 1203,
WALLET_COIN_LINK = 1204, WALLET_COIN_LINK = 1204,
WALLET_COIN_RECOUP_REFRESH = 1206, WALLET_COIN_RECOUP_REFRESH = 1206,
WALLET_AGE_ATTESTATION = 1207,
EXCHANGE_CONFIRM_RECOUP = 1039, EXCHANGE_CONFIRM_RECOUP = 1039,
EXCHANGE_CONFIRM_RECOUP_REFRESH = 1041, EXCHANGE_CONFIRM_RECOUP_REFRESH = 1041,
ANASTASIS_POLICY_UPLOAD = 1400, ANASTASIS_POLICY_UPLOAD = 1400,
@ -807,6 +829,25 @@ export type Edx25519PublicKey = FlavorP<string, "Edx25519PublicKey", 32>;
export type Edx25519PrivateKey = FlavorP<string, "Edx25519PrivateKey", 64>; export type Edx25519PrivateKey = FlavorP<string, "Edx25519PrivateKey", 64>;
export type Edx25519Signature = FlavorP<string, "Edx25519Signature", 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 { export namespace Edx25519 {
const revL = [ const revL = [
0xed, 0xd3, 0xf5, 0x5c, 0x1a, 0x63, 0x12, 0x58, 0xd6, 0x9c, 0xf7, 0xa2, 0xed, 0xd3, 0xf5, 0x5c, 0x1a, 0x63, 0x12, 0x58, 0xd6, 0x9c, 0xf7, 0xa2,
@ -846,9 +887,9 @@ export namespace Edx25519 {
): Promise<OpaqueData> { ): Promise<OpaqueData> {
const res = kdfKw({ const res = kdfKw({
outputLength: 64, outputLength: 64,
salt: stringToBytes("edx2559-derivation"), salt: decodeCrock(seed),
ikm: decodeCrock(pub), ikm: decodeCrock(pub),
info: decodeCrock(seed), info: stringToBytes("edx2559-derivation"),
}); });
return encodeCrock(res); return encodeCrock(res);
@ -860,28 +901,191 @@ export namespace Edx25519 {
): Promise<Edx25519PrivateKey> { ): Promise<Edx25519PrivateKey> {
const pub = await getPublic(priv); const pub = await getPublic(priv);
const privDec = decodeCrock(priv); const privDec = decodeCrock(priv);
const privA = privDec.subarray(0, 32).reverse(); const a = bigintFromNaclArr(privDec.subarray(0, 32));
const a = bigint.fromArray(Array.from(privA), 256, false); 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 newPriv = encodeCrock(
typedArrayConcat([bigintToNaclArr(aPrime, 32), bPrime]),
const aPrime = a.divide(8).multiply(factor).multiply(8);
const bPrime = nacl.hash(
typedArrayConcat([privDec.subarray(32, 64), decodeCrock(factorBuf)]),
); );
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"); throw Error("not implemented");
} }
export function publicKeyDerive( export async function restrictionCommit(
priv: Edx25519PrivateKey, ageMask: number,
seed: OpaqueData, age: number,
): Promise<Edx25519PublicKey> { ): Promise<AgeCommitmentProof> {
throw Error("not implemented") 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"; } from "./time.js";
import { codecForAmountString } from "./amounts.js"; import { codecForAmountString } from "./amounts.js";
import { strcmp } from "./helpers.js"; import { strcmp } from "./helpers.js";
import { Edx25519PublicKey } from "./talerCrypto.js";
/** /**
* Denomination as found in the /keys response from the exchange. * 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. * URL of the exchange this coin was withdrawn from.
*/ */
exchange_url: string; exchange_url: string;
minimum_age_sig?: EddsaSignatureString;
age_commitment?: Edx25519PublicKey[];
} }
/** /**
@ -539,6 +544,8 @@ export interface ContractTerms {
*/ */
max_wire_fee?: string; max_wire_fee?: string;
minimum_age?: number;
/** /**
* Extra data, interpreted by the mechant only. * Extra data, interpreted by the mechant only.
*/ */
@ -957,6 +964,7 @@ export interface ExchangeMeltRequest {
denom_sig: UnblindedSignature; denom_sig: UnblindedSignature;
rc: string; rc: string;
value_with_fee: AmountString; value_with_fee: AmountString;
age_commitment_hash?: HashCodeString;
} }
export interface ExchangeMeltResponse { export interface ExchangeMeltResponse {
@ -1122,7 +1130,7 @@ export type DenominationPubKey = RsaDenominationPubKey | CsDenominationPubKey;
export interface RsaDenominationPubKey { export interface RsaDenominationPubKey {
readonly cipher: DenomKeyType.Rsa; readonly cipher: DenomKeyType.Rsa;
readonly rsa_public_key: string; readonly rsa_public_key: string;
readonly age_mask?: number; readonly age_mask: number;
} }
export interface CsDenominationPubKey { export interface CsDenominationPubKey {
@ -1177,12 +1185,14 @@ export const codecForRsaDenominationPubKey = () =>
buildCodecForObject<RsaDenominationPubKey>() buildCodecForObject<RsaDenominationPubKey>()
.property("cipher", codecForConstString(DenomKeyType.Rsa)) .property("cipher", codecForConstString(DenomKeyType.Rsa))
.property("rsa_public_key", codecForString()) .property("rsa_public_key", codecForString())
.property("age_mask", codecForNumber())
.build("DenominationPubKey"); .build("DenominationPubKey");
export const codecForCsDenominationPubKey = () => export const codecForCsDenominationPubKey = () =>
buildCodecForObject<CsDenominationPubKey>() buildCodecForObject<CsDenominationPubKey>()
.property("cipher", codecForConstString(DenomKeyType.ClauseSchnorr)) .property("cipher", codecForConstString(DenomKeyType.ClauseSchnorr))
.property("cs_public_key", codecForString()) .property("cs_public_key", codecForString())
.property("age_mask", codecForNumber())
.build("CsDenominationPubKey"); .build("CsDenominationPubKey");
export const codecForBankWithdrawalOperationPostResponse = export const codecForBankWithdrawalOperationPostResponse =
@ -1312,6 +1322,7 @@ export const codecForContractTerms = (): Codec<ContractTerms> =>
.property("exchanges", codecForList(codecForExchangeHandle())) .property("exchanges", codecForList(codecForExchangeHandle()))
.property("products", codecOptional(codecForList(codecForProduct()))) .property("products", codecOptional(codecForList(codecForProduct())))
.property("extra", codecForAny()) .property("extra", codecForAny())
.property("minimum_age", codecOptional(codecForNumber()))
.build("ContractTerms"); .build("ContractTerms");
export const codecForMerchantRefundPermission = export const codecForMerchantRefundPermission =
@ -1717,6 +1728,13 @@ export interface ExchangeRefreshRevealRequest {
transfer_pub: EddsaPublicKeyString; transfer_pub: EddsaPublicKeyString;
link_sigs: EddsaSignatureString[]; 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 { export interface DepositSuccess {

View File

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

View File

@ -24,6 +24,7 @@ export interface CoinCoinfigCommon {
feeDeposit: string; feeDeposit: string;
feeRefresh: string; feeRefresh: string;
feeRefund: string; feeRefund: string;
ageRestricted?: boolean;
} }
export interface CoinConfigRsa extends CoinCoinfigCommon { 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_withdraw", c.feeWithdraw);
config.setString(s, "fee_refresh", c.feeRefresh); config.setString(s, "fee_refresh", c.feeRefresh);
config.setString(s, "fee_refund", c.feeRefund); config.setString(s, "fee_refund", c.feeRefund);
if (c.ageRestricted) {
config.setString(s, "age_restricted", "yes");
}
if (c.cipher === "RSA") { if (c.cipher === "RSA") {
config.setString(s, "rsa_keysize", `${c.rsaKeySize}`); config.setString(s, "rsa_keysize", `${c.rsaKeySize}`);
config.setString(s, "cipher", "RSA"); config.setString(s, "cipher", "RSA");
@ -1112,6 +1115,17 @@ export class ExchangeService implements ExchangeServiceInterface {
config.write(this.configFilename); 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() { get masterPub() {
return encodeCrock(this.keyPair.eddsaPub); return encodeCrock(this.keyPair.eddsaPub);
} }
@ -1645,8 +1659,14 @@ export class MerchantService implements MerchantServiceInterface {
await exec(`taler-merchant-dbinit -c "${this.configFilename}"`); await exec(`taler-merchant-dbinit -c "${this.configFilename}"`);
this.proc = this.globalState.spawnService( this.proc = this.globalState.spawnService(
"taler-merchant-httpd", "valgrind",
["-LDEBUG", "-c", this.configFilename, ...this.timetravelArgArr], [
"taler-merchant-httpd",
"-LDEBUG",
"-c",
this.configFilename,
...this.timetravelArgArr,
],
`merchant-${this.merchantConfig.name}`, `merchant-${this.merchantConfig.name}`,
); );
} }
@ -1848,6 +1868,9 @@ export async function runTestWithState(
} }
} catch (e) { } catch (e) {
console.error("FATAL: test failed with exception", e); console.error("FATAL: test failed with exception", e);
if (e instanceof TalerError) {
console.error(`error detail: ${j2s(e.errorDetail)}`);
}
status = "fail"; status = "fail";
} finally { } finally {
await gc.shutdown(); await gc.shutdown();

View File

@ -65,6 +65,13 @@ export interface SimpleTestEnvironment {
wallet: WalletCli; 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 * Run a test case with a simple TESTKUDOS Taler environment, consisting
* of one exchange, one bank and one merchant. * of one exchange, one bank and one merchant.
@ -72,6 +79,7 @@ export interface SimpleTestEnvironment {
export async function createSimpleTestkudosEnvironment( export async function createSimpleTestkudosEnvironment(
t: GlobalTestState, t: GlobalTestState,
coinConfig: CoinConfig[] = defaultCoinConfig.map((x) => x("TESTKUDOS")), coinConfig: CoinConfig[] = defaultCoinConfig.map((x) => x("TESTKUDOS")),
opts: EnvOptions = {},
): Promise<SimpleTestEnvironment> { ): Promise<SimpleTestEnvironment> {
const db = await setupDb(t); const db = await setupDb(t);
@ -108,7 +116,17 @@ export async function createSimpleTestkudosEnvironment(
await bank.pingUntilAvailable(); await bank.pingUntilAvailable();
exchange.addCoinConfigList(coinConfig); 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.start();
await exchange.pingUntilAvailable(); await exchange.pingUntilAvailable();
@ -259,6 +277,7 @@ export async function startWithdrawViaBank(
bank: BankService; bank: BankService;
exchange: ExchangeServiceInterface; exchange: ExchangeServiceInterface;
amount: AmountString; amount: AmountString;
restrictAge?: number;
}, },
): Promise<void> { ): Promise<void> {
const { wallet, bank, exchange, amount } = p; const { wallet, bank, exchange, amount } = p;
@ -270,6 +289,7 @@ export async function startWithdrawViaBank(
await wallet.client.call(WalletApiOperation.GetWithdrawalDetailsForUri, { await wallet.client.call(WalletApiOperation.GetWithdrawalDetailsForUri, {
talerWithdrawUri: wop.taler_withdraw_uri, talerWithdrawUri: wop.taler_withdraw_uri,
restrictAge: p.restrictAge,
}); });
await wallet.runPending(); await wallet.runPending();
@ -279,6 +299,7 @@ export async function startWithdrawViaBank(
await wallet.client.call(WalletApiOperation.AcceptBankIntegratedWithdrawal, { await wallet.client.call(WalletApiOperation.AcceptBankIntegratedWithdrawal, {
exchangeBaseUrl: exchange.baseUrl, exchangeBaseUrl: exchange.baseUrl,
talerWithdrawUri: wop.taler_withdraw_uri, talerWithdrawUri: wop.taler_withdraw_uri,
restrictAge: p.restrictAge,
}); });
// Confirm it // Confirm it
@ -299,6 +320,7 @@ export async function withdrawViaBank(
bank: BankService; bank: BankService;
exchange: ExchangeServiceInterface; exchange: ExchangeServiceInterface;
amount: AmountString; amount: AmountString;
restrictAge?: number;
}, },
): Promise<void> { ): Promise<void> {
const { wallet } = p; 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, shouldLingerInTest,
TestRunResult, TestRunResult,
} from "../harness/harness.js"; } from "../harness/harness.js";
import { runAgeRestrictionsTest } from "./test-age-restrictions.js";
import { runBankApiTest } from "./test-bank-api"; import { runBankApiTest } from "./test-bank-api";
import { runClaimLoopTest } from "./test-claim-loop"; import { runClaimLoopTest } from "./test-claim-loop";
import { runClauseSchnorrTest } from "./test-clause-schnorr.js"; import { runClauseSchnorrTest } from "./test-clause-schnorr.js";
@ -103,6 +104,7 @@ interface TestMainFunction {
} }
const allTests: TestMainFunction[] = [ const allTests: TestMainFunction[] = [
runAgeRestrictionsTest,
runBankApiTest, runBankApiTest,
runClaimLoopTest, runClaimLoopTest,
runClauseSchnorrTest, runClauseSchnorrTest,

View File

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

View File

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

View File

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

View File

@ -40,6 +40,7 @@ import {
CoinEnvelope, CoinEnvelope,
TalerProtocolTimestamp, TalerProtocolTimestamp,
TalerProtocolDuration, TalerProtocolDuration,
AgeCommitmentProof,
} from "@gnu-taler/taler-util"; } from "@gnu-taler/taler-util";
import { RetryInfo } from "./util/retries.js"; import { RetryInfo } from "./util/retries.js";
import { PayCoinSelection } from "./util/coinSelection.js"; import { PayCoinSelection } from "./util/coinSelection.js";
@ -188,6 +189,15 @@ export interface ReserveRecord {
*/ */
bankInfo?: ReserveBankInfo; 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; initialWithdrawalGroupId: string;
/** /**
@ -600,6 +610,8 @@ export interface PlanchetRecord {
coinEv: CoinEnvelope; coinEv: CoinEnvelope;
coinEvHash: string; coinEvHash: string;
ageCommitmentProof?: AgeCommitmentProof;
} }
/** /**
@ -724,6 +736,8 @@ export interface CoinRecord {
* Used to prevent allocation of the same coin for two different payments. * Used to prevent allocation of the same coin for two different payments.
*/ */
allocation?: CoinAllocation; allocation?: CoinAllocation;
ageCommitmentProof?: AgeCommitmentProof;
} }
export interface CoinAllocation { export interface CoinAllocation {
@ -1148,6 +1162,7 @@ export interface WalletContractData {
wireMethod: string; wireMethod: string;
wireInfoHash: string; wireInfoHash: string;
maxDepositFee: AmountJson; maxDepositFee: AmountJson;
minimumAge?: number;
} }
export enum AbortStatus { export enum AbortStatus {

View File

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

View File

@ -26,6 +26,7 @@
*/ */
import { import {
AbsoluteTime, AbsoluteTime,
AgeRestriction,
AmountJson, AmountJson,
Amounts, Amounts,
codecForContractTerms, codecForContractTerms,
@ -197,6 +198,14 @@ export interface CoinSelectionRequest {
maxWireFee: AmountJson; maxWireFee: AmountJson;
maxDepositFee: 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, merchant: parsedContractTerms.merchant,
products: parsedContractTerms.products, products: parsedContractTerms.products,
summaryI18n: parsedContractTerms.summary_i18n, summaryI18n: parsedContractTerms.summary_i18n,
minimumAge: parsedContractTerms.minimum_age,
}; };
} }
@ -825,6 +835,8 @@ async function processDownloadProposalImpl(
proposalResp.sig, proposalResp.sig,
); );
logger.trace(`extracted contract data: ${j2s(contractData)}`);
await ws.db await ws.db
.mktx((x) => ({ proposals: x.proposals, purchases: x.purchases })) .mktx((x) => ({ proposals: x.proposals, purchases: x.purchases }))
.runReadWrite(async (tx) => { .runReadWrite(async (tx) => {
@ -1379,6 +1391,11 @@ export async function generateDepositPermissions(
const { coin, denom } = coinWithDenom[i]; const { coin, denom } = coinWithDenom[i];
let wireInfoHash: string; let wireInfoHash: string;
wireInfoHash = contractData.wireInfoHash; wireInfoHash = contractData.wireInfoHash;
logger.trace(
`signing deposit permission for coin with acp=${j2s(
coin.ageCommitmentProof,
)}`,
);
const dp = await ws.cryptoApi.signDepositPermission({ const dp = await ws.cryptoApi.signDepositPermission({
coinPriv: coin.coinPriv, coinPriv: coin.coinPriv,
coinPub: coin.coinPub, coinPub: coin.coinPub,
@ -1393,6 +1410,8 @@ export async function generateDepositPermissions(
spendAmount: payCoinSel.coinContributions[i], spendAmount: payCoinSel.coinContributions[i],
timestamp: contractData.timestamp, timestamp: contractData.timestamp,
wireInfoHash, wireInfoHash,
ageCommitmentProof: coin.ageCommitmentProof,
requiredMinimumAge: contractData.minimumAge,
}); });
depositPermissions.push(dp); depositPermissions.push(dp);
} }

View File

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

View File

@ -200,6 +200,7 @@ export async function createReserve(
lastError: undefined, lastError: undefined,
currency: req.amount.currency, currency: req.amount.currency,
operationStatus: OperationStatus.Pending, operationStatus: OperationStatus.Pending,
restrictAge: req.restrictAge,
}; };
const exchangeInfo = await updateExchangeFromUrl(ws, req.exchange); const exchangeInfo = await updateExchangeFromUrl(ws, req.exchange);
@ -541,12 +542,9 @@ async function updateReserve(
const reserveUrl = new URL(`reserves/${reservePub}`, reserve.exchangeBaseUrl); const reserveUrl = new URL(`reserves/${reservePub}`, reserve.exchangeBaseUrl);
reserveUrl.searchParams.set("timeout_ms", "200"); reserveUrl.searchParams.set("timeout_ms", "200");
const resp = await ws.http.get( const resp = await ws.http.get(reserveUrl.href, {
reserveUrl.href, timeout: getReserveRequestTimeout(reserve),
{ });
timeout: getReserveRequestTimeout(reserve),
},
);
const result = await readSuccessResponseJsonOrErrorCode( const result = await readSuccessResponseJsonOrErrorCode(
resp, resp,
@ -632,17 +630,12 @@ async function updateReserve(
amountReservePlus, amountReservePlus,
amountReserveMinus, amountReserveMinus,
).amount; ).amount;
const denomSel = selectWithdrawalDenominations( const denomSel = selectWithdrawalDenominations(remainingAmount, denoms);
remainingAmount,
denoms,
);
logger.trace( logger.trace(
`Remaining unclaimed amount in reseve is ${Amounts.stringify( `Remaining unclaimed amount in reseve is ${Amounts.stringify(
remainingAmount, remainingAmount,
)} and can be withdrawn with ${ )} and can be withdrawn with ${denomSel.selectedDenoms.length} coins`,
denomSel.selectedDenoms.length
} coins`,
); );
if (denomSel.selectedDenoms.length === 0) { if (denomSel.selectedDenoms.length === 0) {
@ -759,6 +752,7 @@ export async function createTalerWithdrawReserve(
selectedExchange: string, selectedExchange: string,
options: { options: {
forcedDenomSel?: ForcedDenomSel; forcedDenomSel?: ForcedDenomSel;
restrictAge?: number;
} = {}, } = {},
): Promise<AcceptWithdrawalResponse> { ): Promise<AcceptWithdrawalResponse> {
await updateExchangeFromUrl(ws, selectedExchange); await updateExchangeFromUrl(ws, selectedExchange);
@ -774,6 +768,7 @@ export async function createTalerWithdrawReserve(
exchange: selectedExchange, exchange: selectedExchange,
senderWire: withdrawInfo.senderWire, senderWire: withdrawInfo.senderWire,
exchangePaytoUri: exchangePaytoUri, exchangePaytoUri: exchangePaytoUri,
restrictAge: options.restrictAge,
}); });
// We do this here, as the reserve should be registered before we return, // 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. // 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, cipher: DenomKeyType.Rsa,
rsa_public_key: rsa_public_key:
"040000XT67C8KBD6B75TTQ3SK8FWXMNQW4372T3BDDGPAMB9RFCA03638W8T3F71WFEFK9NP32VKYVNFXPYRWQ1N1HDKV5J0DFEKHBPJCYSWCBJDRNWD7G8BN8PT97FA9AMV75MYEK4X54D1HGJ207JSVJBGFCATSPNTEYNHEQF1F220W00TBZR1HNPDQFD56FG0DJQ9KGHM8EC33H6AY9YN9CNX5R3Z4TZ4Q23W47SBHB13H6W74FQJG1F50X38VRSC4SR8RWBAFB7S4K8D2H4NMRFSQT892A3T0BTBW7HM5C0H2CK6FRKG31F7W9WP1S29013K5CXYE55CT8TH6N8J9B780R42Y5S3ZB6J6E9H76XBPSGH4TGYSR2VZRB98J417KCQMZKX1BB67E7W5KVE37TC9SJ904002", "040000XT67C8KBD6B75TTQ3SK8FWXMNQW4372T3BDDGPAMB9RFCA03638W8T3F71WFEFK9NP32VKYVNFXPYRWQ1N1HDKV5J0DFEKHBPJCYSWCBJDRNWD7G8BN8PT97FA9AMV75MYEK4X54D1HGJ207JSVJBGFCATSPNTEYNHEQF1F220W00TBZR1HNPDQFD56FG0DJQ9KGHM8EC33H6AY9YN9CNX5R3Z4TZ4Q23W47SBHB13H6W74FQJG1F50X38VRSC4SR8RWBAFB7S4K8D2H4NMRFSQT892A3T0BTBW7HM5C0H2CK6FRKG31F7W9WP1S29013K5CXYE55CT8TH6N8J9B780R42Y5S3ZB6J6E9H76XBPSGH4TGYSR2VZRB98J417KCQMZKX1BB67E7W5KVE37TC9SJ904002",
age_mask: 0,
}, },
denomPubHash: denomPubHash:
"Q21FQSSG4FXNT96Z14CHXM8N1RZAG9GPHAV8PRWS0PZAAVWH7PBW6R97M2CH19KKP65NNSWXY7B6S53PT3CBM342E357ZXDDJ8RDVW8", "Q21FQSSG4FXNT96Z14CHXM8N1RZAG9GPHAV8PRWS0PZAAVWH7PBW6R97M2CH19KKP65NNSWXY7B6S53PT3CBM342E357ZXDDJ8RDVW8",
@ -86,6 +87,7 @@ test("withdrawal selection bug repro", (t) => {
cipher: DenomKeyType.Rsa, cipher: DenomKeyType.Rsa,
rsa_public_key: rsa_public_key:
"040000Y63CF78QFPKRY77BRK9P557Q1GQWX3NCZ3HSYSK0Z7TT0KGRA7N4SKBKEHSTVHX1Z9DNXMJR4EXSY1TXCKV0GJ3T3YYC6Z0JNMJFVYQAV4FX5J90NZH1N33MZTV8HS9SMNAA9S6K73G4P99GYBB01B0P6M1KXZ5JRDR7VWBR3MEJHHGJ6QBMCJR3NWJRE3WJW9PRY8QPQ2S7KFWTWRESH2DBXCXWBD2SRN6P9YX8GRAEMFEGXC9V5GVJTEMH6ZDGNXFPWZE3JVJ2Q4N9GDYKBCHZCJ7M7M2RJ9ZV4Y64NAN9BT6XDC68215GKKRHTW1BBF1MYY6AR3JCTT9HYAM923RMVQR3TAEB7SDX8J76XRZWYH3AGJCZAQGMN5C8SSH9AHQ9RNQJQ15CN45R37X4YNFJV904002", "040000Y63CF78QFPKRY77BRK9P557Q1GQWX3NCZ3HSYSK0Z7TT0KGRA7N4SKBKEHSTVHX1Z9DNXMJR4EXSY1TXCKV0GJ3T3YYC6Z0JNMJFVYQAV4FX5J90NZH1N33MZTV8HS9SMNAA9S6K73G4P99GYBB01B0P6M1KXZ5JRDR7VWBR3MEJHHGJ6QBMCJR3NWJRE3WJW9PRY8QPQ2S7KFWTWRESH2DBXCXWBD2SRN6P9YX8GRAEMFEGXC9V5GVJTEMH6ZDGNXFPWZE3JVJ2Q4N9GDYKBCHZCJ7M7M2RJ9ZV4Y64NAN9BT6XDC68215GKKRHTW1BBF1MYY6AR3JCTT9HYAM923RMVQR3TAEB7SDX8J76XRZWYH3AGJCZAQGMN5C8SSH9AHQ9RNQJQ15CN45R37X4YNFJV904002",
age_mask: 0,
}, },
denomPubHash: denomPubHash:
@ -141,6 +143,7 @@ test("withdrawal selection bug repro", (t) => {
cipher: DenomKeyType.Rsa, cipher: DenomKeyType.Rsa,
rsa_public_key: rsa_public_key:
"040000YDESWC2B962DA4WK356SC50MA3N9KV0ZSGY3RC48JCTY258W909C7EEMT5BTC5KZ5T4CERCZ141P9QF87EK2BD1XEEM5GB07MB3H19WE4CQGAS8X84JBWN83PQGQXVMWE5HFA992KMGHC566GT9ZS2QPHZB6X89C4A80Z663PYAAPXP728VHAKATGNNBQ01ZZ2XD1CH9Y38YZBSPJ4K7GB2J76GBCYAVD9ENHDVWXJAXYRPBX4KSS5TXRR3K5NEN9ZV3AJD2V65K7ABRZDF5D5V1FJZZMNJ5XZ4FEREEKEBV9TDFPGJTKDEHEC60K3DN24DAATRESDJ1ZYYSYSRCAT4BT2B62ARGVMJTT5N2R126DRW9TGRWCW0ZAF2N2WET1H4NJEW77X0QT46Z5R3MZ0XPHD04002", "040000YDESWC2B962DA4WK356SC50MA3N9KV0ZSGY3RC48JCTY258W909C7EEMT5BTC5KZ5T4CERCZ141P9QF87EK2BD1XEEM5GB07MB3H19WE4CQGAS8X84JBWN83PQGQXVMWE5HFA992KMGHC566GT9ZS2QPHZB6X89C4A80Z663PYAAPXP728VHAKATGNNBQ01ZZ2XD1CH9Y38YZBSPJ4K7GB2J76GBCYAVD9ENHDVWXJAXYRPBX4KSS5TXRR3K5NEN9ZV3AJD2V65K7ABRZDF5D5V1FJZZMNJ5XZ4FEREEKEBV9TDFPGJTKDEHEC60K3DN24DAATRESDJ1ZYYSYSRCAT4BT2B62ARGVMJTT5N2R126DRW9TGRWCW0ZAF2N2WET1H4NJEW77X0QT46Z5R3MZ0XPHD04002",
age_mask: 0,
}, },
denomPubHash: denomPubHash:
"JS61DTKAFM0BX8Q4XV3ZSKB921SM8QK745Z2AFXTKFMBHHFNBD8TQ5ETJHFNDGBGX22FFN2A2ERNYG1SGSDQWNQHQQ2B14DBVJYJG8R", "JS61DTKAFM0BX8Q4XV3ZSKB921SM8QK745Z2AFXTKFMBHHFNBD8TQ5ETJHFNDGBGX22FFN2A2ERNYG1SGSDQWNQHQQ2B14DBVJYJG8R",
@ -195,6 +198,7 @@ test("withdrawal selection bug repro", (t) => {
cipher: DenomKeyType.Rsa, cipher: DenomKeyType.Rsa,
rsa_public_key: rsa_public_key:
"040000YG3T1ADB8DVA6BD3EPV6ZHSHTDW35DEN4VH1AE6CSB7P1PSDTNTJG866PHF6QB1CCWYCVRGA0FVBJ9Q0G7KV7AD9010GDYBQH0NNPHW744MTNXVXWBGGGRGQGYK4DTYN1DSWQ1FZNDSZZPB5BEKG2PDJ93NX2JTN06Y8QMS2G734Z9XHC10EENBG2KVB7EJ3CM8PV1T32RC7AY62F3496E8D8KRHJQQTT67DSGMNKK86QXVDTYW677FG27DP20E8XY3M6FQD53NDJ1WWES91401MV1A3VXVPGC76GZVDD62W3WTJ1YMKHTTA3MRXX3VEAAH3XTKDN1ER7X6CZPMYTF8VK735VP2B2TZGTF28TTW4FZS32SBS64APCDF6SZQ427N5538TJC7SRE71YSP5ET8GS904002", "040000YG3T1ADB8DVA6BD3EPV6ZHSHTDW35DEN4VH1AE6CSB7P1PSDTNTJG866PHF6QB1CCWYCVRGA0FVBJ9Q0G7KV7AD9010GDYBQH0NNPHW744MTNXVXWBGGGRGQGYK4DTYN1DSWQ1FZNDSZZPB5BEKG2PDJ93NX2JTN06Y8QMS2G734Z9XHC10EENBG2KVB7EJ3CM8PV1T32RC7AY62F3496E8D8KRHJQQTT67DSGMNKK86QXVDTYW677FG27DP20E8XY3M6FQD53NDJ1WWES91401MV1A3VXVPGC76GZVDD62W3WTJ1YMKHTTA3MRXX3VEAAH3XTKDN1ER7X6CZPMYTF8VK735VP2B2TZGTF28TTW4FZS32SBS64APCDF6SZQ427N5538TJC7SRE71YSP5ET8GS904002",
age_mask: 0,
}, },
denomPubHash: denomPubHash:
@ -250,6 +254,7 @@ test("withdrawal selection bug repro", (t) => {
cipher: DenomKeyType.Rsa, cipher: DenomKeyType.Rsa,
rsa_public_key: rsa_public_key:
"040000ZC0G60E9QQ5PD81TSDWD9GV5Y6P8Z05NSPA696DP07NGQQVSRQXBA76Q6PRB0YFX295RG4MTQJXAZZ860ET307HSC2X37XAVGQXRVB8Q4F1V7NP5ZEVKTX75DZK1QRAVHEZGQYKSSH6DBCJNQF6V9WNQF3GEYVA4KCBHA7JF772KHXM9642C28Z0AS4XXXV2PABAN5C8CHYD5H7JDFNK3920W5Q69X0BS84XZ4RE2PW6HM1WZ6KGZ3MKWWWCPKQ1FSFABRBWKAB09PF563BEBXKY6M38QETPH5EDWGANHD0SC3QV0WXYVB7BNHNNQ0J5BNV56K563SYHM4E5ND260YRJSYA1GN5YSW2B1J5T1A1EBNYF2DN6JNJKWXWEQ42G5YS17ZSZ5EWDRA9QKV8EGTCNAD04002", "040000ZC0G60E9QQ5PD81TSDWD9GV5Y6P8Z05NSPA696DP07NGQQVSRQXBA76Q6PRB0YFX295RG4MTQJXAZZ860ET307HSC2X37XAVGQXRVB8Q4F1V7NP5ZEVKTX75DZK1QRAVHEZGQYKSSH6DBCJNQF6V9WNQF3GEYVA4KCBHA7JF772KHXM9642C28Z0AS4XXXV2PABAN5C8CHYD5H7JDFNK3920W5Q69X0BS84XZ4RE2PW6HM1WZ6KGZ3MKWWWCPKQ1FSFABRBWKAB09PF563BEBXKY6M38QETPH5EDWGANHD0SC3QV0WXYVB7BNHNNQ0J5BNV56K563SYHM4E5ND260YRJSYA1GN5YSW2B1J5T1A1EBNYF2DN6JNJKWXWEQ42G5YS17ZSZ5EWDRA9QKV8EGTCNAD04002",
age_mask: 0,
}, },
denomPubHash: denomPubHash:
"A41HW0Q2H9PCNMEWW0C0N45QAYVXZ8SBVRRAHE4W6X24SV1TH38ANTWDT80JXEBW9Z8PVPGT9GFV2EYZWJ5JW5W1N34NFNKHQSZ1PFR", "A41HW0Q2H9PCNMEWW0C0N45QAYVXZ8SBVRRAHE4W6X24SV1TH38ANTWDT80JXEBW9Z8PVPGT9GFV2EYZWJ5JW5W1N34NFNKHQSZ1PFR",
@ -304,6 +309,7 @@ test("withdrawal selection bug repro", (t) => {
cipher: DenomKeyType.Rsa, cipher: DenomKeyType.Rsa,
rsa_public_key: rsa_public_key:
"040000ZSK2PMVY6E3NBQ52KXMW029M60F4BWYTDS0FZSD0PE53CNZ9H6TM3GQK1WRTEKQ5GRWJ1J9DY6Y42SP47QVT1XD1G0W05SQ5F3F7P5KSWR0FJBJ9NZBXQEVN8Q4JRC94X3JJ3XV3KBYTZ2HTDFV28C3H2SRR0XGNZB4FY85NDZF1G4AEYJJ9QB3C0V8H70YB8RV3FKTNH7XS4K4HFNZHJ5H9VMX5SM9Z2DX37HA5WFH0E2MJBVVF2BWWA5M0HPPSB365RAE2AMD42Q65A96WD80X27SB2ZNQZ8WX0K13FWF85GZ6YNYAJGE1KGN06JDEKE9QD68Z651D7XE8V6664TVVC8M68S7WD0DSXMJQKQ0BNJXNDE29Q7MRX6DA3RW0PZ44B3TKRK0294FPVZTNSTA6XF04002", "040000ZSK2PMVY6E3NBQ52KXMW029M60F4BWYTDS0FZSD0PE53CNZ9H6TM3GQK1WRTEKQ5GRWJ1J9DY6Y42SP47QVT1XD1G0W05SQ5F3F7P5KSWR0FJBJ9NZBXQEVN8Q4JRC94X3JJ3XV3KBYTZ2HTDFV28C3H2SRR0XGNZB4FY85NDZF1G4AEYJJ9QB3C0V8H70YB8RV3FKTNH7XS4K4HFNZHJ5H9VMX5SM9Z2DX37HA5WFH0E2MJBVVF2BWWA5M0HPPSB365RAE2AMD42Q65A96WD80X27SB2ZNQZ8WX0K13FWF85GZ6YNYAJGE1KGN06JDEKE9QD68Z651D7XE8V6664TVVC8M68S7WD0DSXMJQKQ0BNJXNDE29Q7MRX6DA3RW0PZ44B3TKRK0294FPVZTNSTA6XF04002",
age_mask: 0,
}, },
denomPubHash: denomPubHash:
"F5NGBX33DTV4595XZZVK0S2MA1VMXFEJQERE5EBP5DS4QQ9EFRANN7YHWC1TKSHT2K6CQWDBRES8D3DWR0KZF5RET40B4AZXZ0RW1ZG", "F5NGBX33DTV4595XZZVK0S2MA1VMXFEJQERE5EBP5DS4QQ9EFRANN7YHWC1TKSHT2K6CQWDBRES8D3DWR0KZF5RET40B4AZXZ0RW1ZG",

View File

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

View File

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

View File

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