summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--packages/taler-util/src/nacl-fast.ts33
-rw-r--r--packages/taler-util/src/talerCrypto.test.ts86
-rw-r--r--packages/taler-util/src/talerCrypto.ts236
-rw-r--r--packages/taler-util/src/talerTypes.ts20
-rw-r--r--packages/taler-util/src/walletTypes.ts15
-rw-r--r--packages/taler-wallet-cli/src/harness/denomStructures.ts1
-rw-r--r--packages/taler-wallet-cli/src/harness/harness.ts27
-rw-r--r--packages/taler-wallet-cli/src/harness/helpers.ts24
-rw-r--r--packages/taler-wallet-cli/src/integrationtests/test-age-restrictions.ts64
-rw-r--r--packages/taler-wallet-cli/src/integrationtests/testrunner.ts2
-rw-r--r--packages/taler-wallet-core/src/crypto/cryptoImplementation.ts76
-rw-r--r--packages/taler-wallet-core/src/crypto/cryptoTypes.ts2
-rw-r--r--packages/taler-wallet-core/src/crypto/workers/cryptoDispatcher.ts4
-rw-r--r--packages/taler-wallet-core/src/db.ts15
-rw-r--r--packages/taler-wallet-core/src/operations/exchanges.ts2
-rw-r--r--packages/taler-wallet-core/src/operations/pay.ts19
-rw-r--r--packages/taler-wallet-core/src/operations/refresh.ts24
-rw-r--r--packages/taler-wallet-core/src/operations/reserves.ts21
-rw-r--r--packages/taler-wallet-core/src/operations/withdraw.test.ts6
-rw-r--r--packages/taler-wallet-core/src/operations/withdraw.ts15
-rw-r--r--packages/taler-wallet-core/src/util/coinSelection.test.ts1
-rw-r--r--packages/taler-wallet-core/src/wallet.ts3
22 files changed, 630 insertions, 66 deletions
diff --git a/packages/taler-util/src/nacl-fast.ts b/packages/taler-util/src/nacl-fast.ts
index 82bdc7cec..c45674bef 100644
--- a/packages/taler-util/src/nacl-fast.ts
+++ b/packages/taler-util/src/nacl-fast.ts
@@ -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(
diff --git a/packages/taler-util/src/talerCrypto.test.ts b/packages/taler-util/src/talerCrypto.test.ts
index 70ad8a614..5e8f37d80 100644
--- a/packages/taler-util/src/talerCrypto.test.ts
+++ b/packages/taler-util/src/talerCrypto.test.ts
@@ -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);
+});
diff --git a/packages/taler-util/src/talerCrypto.ts b/packages/taler-util/src/talerCrypto.ts
index 282d22d8b..228dc3269 100644
--- a/packages/taler-util/src/talerCrypto.ts
+++ b/packages/taler-util/src/talerCrypto.ts
@@ -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 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 newPriv = encodeCrock(
+ typedArrayConcat([bigintToNaclArr(aPrime, 32), bPrime]),
+ );
+
+ return newPriv;
+ }
- const factorBuf = await deriveFactor(pub, seed);
+ 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);
+ }
+}
- const factor = bigint.fromArray(Array.from(factorBuf), 256, false);
+export interface AgeCommitment {
+ mask: number;
- const aPrime = a.divide(8).multiply(factor).multiply(8);
+ /**
+ * Public keys, one for each age group specified in the age mask.
+ */
+ publicKeys: Edx25519PublicKey[];
+}
- const bPrime = nacl.hash(
- typedArrayConcat([privDec.subarray(32, 64), decodeCrock(factorBuf)]),
- );
+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[];
+}
- Uint8Array.from(aPrime.toArray(256).value)
+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");
}
}
diff --git a/packages/taler-util/src/talerTypes.ts b/packages/taler-util/src/talerTypes.ts
index b1bf6ab38..abac1cd12 100644
--- a/packages/taler-util/src/talerTypes.ts
+++ b/packages/taler-util/src/talerTypes.ts
@@ -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 {
diff --git a/packages/taler-util/src/walletTypes.ts b/packages/taler-util/src/walletTypes.ts
index 818ba37fe..e094bc385 100644
--- a/packages/taler-util/src/walletTypes.ts
+++ b/packages/taler-util/src/walletTypes.ts
@@ -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 {
diff --git a/packages/taler-wallet-cli/src/harness/denomStructures.ts b/packages/taler-wallet-cli/src/harness/denomStructures.ts
index 2ca777030..b12857c7e 100644
--- a/packages/taler-wallet-cli/src/harness/denomStructures.ts
+++ b/packages/taler-wallet-cli/src/harness/denomStructures.ts
@@ -24,6 +24,7 @@ export interface CoinCoinfigCommon {
feeDeposit: string;
feeRefresh: string;
feeRefund: string;
+ ageRestricted?: boolean;
}
export interface CoinConfigRsa extends CoinCoinfigCommon {
diff --git a/packages/taler-wallet-cli/src/harness/harness.ts b/packages/taler-wallet-cli/src/harness/harness.ts
index 30503e488..a2339e5f3 100644
--- a/packages/taler-wallet-cli/src/harness/harness.ts
+++ b/packages/taler-wallet-cli/src/harness/harness.ts
@@ -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(
- "taler-merchant-httpd",
- ["-LDEBUG", "-c", this.configFilename, ...this.timetravelArgArr],
+ "valgrind",
+ [
+ "taler-merchant-httpd",
+ "-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();
diff --git a/packages/taler-wallet-cli/src/harness/helpers.ts b/packages/taler-wallet-cli/src/harness/helpers.ts
index 3840dcf94..db66efbb6 100644
--- a/packages/taler-wallet-cli/src/harness/helpers.ts
+++ b/packages/taler-wallet-cli/src/harness/helpers.ts
@@ -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();
- 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.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;
diff --git a/packages/taler-wallet-cli/src/integrationtests/test-age-restrictions.ts b/packages/taler-wallet-cli/src/integrationtests/test-age-restrictions.ts
new file mode 100644
index 000000000..9f523ae5d
--- /dev/null
+++ b/packages/taler-wallet-cli/src/integrationtests/test-age-restrictions.ts
@@ -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"];
diff --git a/packages/taler-wallet-cli/src/integrationtests/testrunner.ts b/packages/taler-wallet-cli/src/integrationtests/testrunner.ts
index d8dc569d2..dcbf84497 100644
--- a/packages/taler-wallet-cli/src/integrationtests/testrunner.ts
+++ b/packages/taler-wallet-cli/src/integrationtests/testrunner.ts
@@ -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,
diff --git a/packages/taler-wallet-core/src/crypto/cryptoImplementation.ts b/packages/taler-wallet-core/src/crypto/cryptoImplementation.ts
index fa754e354..052d50ca7 100644
--- a/packages/taler-wallet-core/src/crypto/cryptoImplementation.ts
+++ b/packages/taler-wallet-core/src/crypto/cryptoImplementation.ts
@@ -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))
diff --git a/packages/taler-wallet-core/src/crypto/cryptoTypes.ts b/packages/taler-wallet-core/src/crypto/cryptoTypes.ts
index deff15071..fe5dbcec6 100644
--- a/packages/taler-wallet-core/src/crypto/cryptoTypes.ts
+++ b/packages/taler-wallet-core/src/crypto/cryptoTypes.ts
@@ -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;
}
diff --git a/packages/taler-wallet-core/src/crypto/workers/cryptoDispatcher.ts b/packages/taler-wallet-core/src/crypto/workers/cryptoDispatcher.ts
index f6c8ae61e..2ef0d7c69 100644
--- a/packages/taler-wallet-core/src/crypto/workers/cryptoDispatcher.ts
+++ b/packages/taler-wallet-core/src/crypto/workers/cryptoDispatcher.ts
@@ -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) {
diff --git a/packages/taler-wallet-core/src/db.ts b/packages/taler-wallet-core/src/db.ts
index e3da35975..0a1b40d2a 100644
--- a/packages/taler-wallet-core/src/db.ts
+++ b/packages/taler-wallet-core/src/db.ts
@@ -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 {
diff --git a/packages/taler-wallet-core/src/operations/exchanges.ts b/packages/taler-wallet-core/src/operations/exchanges.ts
index 26bca8c14..39edd6307 100644
--- a/packages/taler-wallet-core/src/operations/exchanges.ts
+++ b/packages/taler-wallet-core/src/operations/exchanges.ts
@@ -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(
diff --git a/packages/taler-wallet-core/src/operations/pay.ts b/packages/taler-wallet-core/src/operations/pay.ts
index fa36c724f..a1773547a 100644
--- a/packages/taler-wallet-core/src/operations/pay.ts
+++ b/packages/taler-wallet-core/src/operations/pay.ts
@@ -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);
}
diff --git a/packages/taler-wallet-core/src/operations/refresh.ts b/packages/taler-wallet-core/src/operations/refresh.ts
index 10584fb94..215676118 100644
--- a/packages/taler-wallet-core/src/operations/refresh.ts
+++ b/packages/taler-wallet-core/src/operations/refresh.ts
@@ -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}`);
diff --git a/packages/taler-wallet-core/src/operations/reserves.ts b/packages/taler-wallet-core/src/operations/reserves.ts
index ff09d1a50..8e606bd60 100644
--- a/packages/taler-wallet-core/src/operations/reserves.ts
+++ b/packages/taler-wallet-core/src/operations/reserves.ts
@@ -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,
- {
- timeout: getReserveRequestTimeout(reserve),
- },
- );
+ 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.
diff --git a/packages/taler-wallet-core/src/operations/withdraw.test.ts b/packages/taler-wallet-core/src/operations/withdraw.test.ts
index e5894a3e7..9f9146719 100644
--- a/packages/taler-wallet-core/src/operations/withdraw.test.ts
+++ b/packages/taler-wallet-core/src/operations/withdraw.test.ts
@@ -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",
diff --git a/packages/taler-wallet-core/src/operations/withdraw.ts b/packages/taler-wallet-core/src/operations/withdraw.ts
index d4ca58401..94f8e20b9 100644
--- a/packages/taler-wallet-core/src/operations/withdraw.ts
+++ b/packages/taler-wallet-core/src/operations/withdraw.ts
@@ -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);
diff --git a/packages/taler-wallet-core/src/util/coinSelection.test.ts b/packages/taler-wallet-core/src/util/coinSelection.test.ts
index 1675a9a35..dc64a57dc 100644
--- a/packages/taler-wallet-core/src/util/coinSelection.test.ts
+++ b/packages/taler-wallet-core/src/util/coinSelection.test.ts
@@ -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/",
diff --git a/packages/taler-wallet-core/src/wallet.ts b/packages/taler-wallet-core/src/wallet.ts
index e17bbb805..96722aefb 100644
--- a/packages/taler-wallet-core/src/wallet.ts
+++ b/packages/taler-wallet-core/src/wallet.ts
@@ -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(