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