taler-typescript-core

Wallet core logic and WebUIs for various components
Log | Files | Refs | Submodules | README | LICENSE

commit 1061309d9d50f19826449d9b00c7bb7b00edda57
parent 6337ac7b784d6556ebeb2dad3021ce40732511b9
Author: Florian Dold <dold@taler.net>
Date:   Wed, 29 Jul 2026 12:08:59 +0200

util: check that "forever" and "never" encode as UINT64_MAX

The big-integer fallback path is silent when it disagrees with setBigUint64,
so it is pinned against the native one rather than only by value.

Issue: https://bugs.taler.net/n/11561

Diffstat:
Mpackages/taler-util/src/taler-crypto.test.ts | 68+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
1 file changed, 67 insertions(+), 1 deletion(-)

diff --git a/packages/taler-util/src/taler-crypto.test.ts b/packages/taler-util/src/taler-crypto.test.ts @@ -57,8 +57,11 @@ import { bufferForUint32, createEddsaKeyPair, decryptContractForDeposit, + durationRoundedToBuffer, encryptContractForDeposit, encryptWithDerivedKey, + timestampRoundedToBuffer, + toHexString, typedArrayConcat, } from "./taler-crypto.js"; import * as fflate from "fflate"; @@ -66,7 +69,7 @@ import { canonicalJson } from "./helpers.js"; import { randomBytes } from "./nacl-fast.js"; import * as talerCrypto from "./taler-crypto.js"; import { signAmlDecision } from "./taler-signatures.js"; -import { TalerProtocolTimestamp } from "./time.js"; +import { TalerProtocolDuration, TalerProtocolTimestamp } from "./time.js"; import { sha512 } from "./kdf.js"; import * as nacl from "./nacl-fast.js"; import { initNodePrng } from "./prng-node.js"; @@ -770,3 +773,66 @@ test("contract decryption honours the declared plaintext length", async (t) => { ); } }); + +test("times are signed over as big-endian microseconds", (t) => { + assert.strictEqual( + toHexString(durationRoundedToBuffer({ d_us: 3600000000 })), + "00000000d693a400", + ); + // Timestamps travel in seconds but are signed over in microseconds, since + // that is what struct GNUNET_TIME_AbsoluteNBO holds. + assert.strictEqual( + toHexString( + timestampRoundedToBuffer(TalerProtocolTimestamp.fromSeconds(1767225600)), + ), + "0006474846204000", + ); +}); + +test("'forever' and 'never' are signed over as UINT64_MAX", (t) => { + // GNUNET_TIME_UNIT_FOREVER_REL and _ABS are UINT64_MAX, so any other + // encoding here breaks every signature over a field the exchange left + // unbounded — the global fees' purse_timeout and history_expiration are the + // ones a wallet meets in practice. + assert.strictEqual( + toHexString(durationRoundedToBuffer(TalerProtocolDuration.forever())), + "ffffffffffffffff", + ); + assert.strictEqual( + toHexString(timestampRoundedToBuffer(TalerProtocolTimestamp.never())), + "ffffffffffffffff", + ); +}); + +test("the big-integer fallback encodes times like setBigUint64 does", (t) => { + // Runtimes without DataView.prototype.setBigUint64 take a separate branch, + // and a disagreement there is silent: nothing throws, the wallet just signs + // a different blob than the exchange did. + const durations: TalerProtocolDuration[] = [ + TalerProtocolDuration.forever(), + { d_us: 0 }, + { d_us: 3600000000 }, + ]; + const timestamps: TalerProtocolTimestamp[] = [ + TalerProtocolTimestamp.never(), + TalerProtocolTimestamp.fromSeconds(0), + TalerProtocolTimestamp.fromSeconds(1767225600), + ]; + const encodeAll = (): Uint8Array[] => [ + ...durations.map((d) => durationRoundedToBuffer(d)), + ...timestamps.map((ts) => timestampRoundedToBuffer(ts)), + ]; + + const native = encodeAll(); + const setBigUint64 = Object.getOwnPropertyDescriptor( + DataView.prototype, + "setBigUint64", + ); + assert.ok(setBigUint64); + delete (DataView.prototype as any).setBigUint64; + try { + assert.deepStrictEqual(encodeAll(), native); + } finally { + Object.defineProperty(DataView.prototype, "setBigUint64", setBigUint64); + } +});