From b89497639f733749bd5fdc464f99d917def44b5e Mon Sep 17 00:00:00 2001 From: Phlosioneer Date: Mon, 26 Aug 2024 02:59:51 -0400 Subject: [PATCH] Fix bugs in bn.halveM and bn.montpowermod `montpowermod` was incorrectly assuming that the base value was already modulo N. This went undetected for so long because the `montMul` function is VERY robust against unusually-large bases. As long as the base was within ~8 bits of the modulus, the algorithm would perform correctly. It was also partly masked by the second issue in halveM. The halveM function did not handle halving 0 or 1 correctly. It would pop the last limb from the array, returning a `bn` in a weird state. Most other `bn` operations could recover from it, but the check that `montpowermod` performs on the modulus is particularly sensitive to the bug. On my machine this issue was related to the montpowermod, so I have included both bugs together in the same commit. See below. I suspect that there is another issue in the montpowermod code, it doesn't call `normalize()` after `RP.add(NN)` and `NP.add(RR)`, while the `halveM()` call assumes its input is normalized. I haven't actually encountered erroneous output, though. ------ On the windows build node v20.10.0, the jit version of halveM was behaving differently from the interpreted version (i.e. while step debugging, and while the jit was cold), for the specific input `new bn(2000).powermod(800, 19)`, evaluating halveM on line: ```js while (RT.greaterEquals(1)) { RT.halveM(); ``` This would cause the extended Euclidean algorithm check to fail reverting to the slower (correct) powermod code. Powermod bug was introduced in commit 2f591b4 HalveM bug was introduced in commit c08108f Closes #419 --- core/bn.js | 6 ++++-- test/bn_test.js | 18 ++++++++++++++++++ test/bn_vectors.js | 6 ++++++ 3 files changed, 28 insertions(+), 2 deletions(-) diff --git a/core/bn.js b/core/bn.js index 3d5acc5a..52eb23e8 100644 --- a/core/bn.js +++ b/core/bn.js @@ -140,7 +140,7 @@ sjcl.bn.prototype = { l[i] = (tmp+carry)>>1; carry = (tmp&1) << r; } - if (!l[l.length-1]) { + if (!l[l.length-1] && l.length > 1) { l.pop(); } return this; @@ -386,6 +386,7 @@ sjcl.bn.prototype = { var montIn = function(c) { return montMul(c, R2); }, montMul = function(a, b) { // Standard Montgomery reduction + // Assumes a and b are already mod N var k, ab, right, abBar, mask = (1 << (s + 1)) - 1; ab = a.mul(b); @@ -419,7 +420,8 @@ sjcl.bn.prototype = { }, montOut = function(c) { return montMul(c, 1); }; - pow = montIn(pow); + // pow may be larger than N, and montIn/montMul assume their params are already mod N. + pow = montIn(pow.mod(N)); out = montIn(out); // Sliding-Window Exponentiation (HAC 14.85) diff --git a/test/bn_test.js b/test/bn_test.js index 37ec13f5..b03f8ea9 100644 --- a/test/bn_test.js +++ b/test/bn_test.js @@ -95,3 +95,21 @@ new sjcl.test.TestCase("Big Limbs Multiplication test", function (cb) { cb && cb(); }); + + +new sjcl.test.TestCase("Regression: Halving one makes zero", function (cb) { + if (!sjcl.bn) { + this.unimplemented(); + cb && cb(); + return; + } + + var actual = new sjcl.bn(1).halveM(); + this.require(actual.limbs.length === 1, "Expected: " + [0] + " Actual: " + actual.limbs); + this.require(actual.limbs[0] === 0, "Expected: " + [0] + " Actual: " + actual.limbs); + + actual = new sjcl.bn(0).halveM(); + this.require(actual.limbs.length === 1, "Expected: " + [0] + " Actual: " + actual.limbs); + this.require(actual.limbs[0] === 0, "Expected: " + [0] + " Actual: " + actual.limbs); + cb && cb(); +}); diff --git a/test/bn_vectors.js b/test/bn_vectors.js index 6d18d51e..16687dfd 100644 --- a/test/bn_vectors.js +++ b/test/bn_vectors.js @@ -78,5 +78,11 @@ sjcl.test.vector.bn_powermod = [ x: "eeaf0ab9adb3008dd6c314c9c25600057674df692c0006e0d5d8e2050b98be48e4", N: "b48130d6e07674df740e1d33b4816e0d5d8e20e2050b98be48e457674df74096ea", v: "9c3219b694befb9caac51a13eb1ac7053b02c654b6a0541cfa60c483592d478630" + }, + { + g: 2000, + x: 2, + N: 19, + v: 6 } ];