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:
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);
+ }
+});