Skip to content

Commit 9c9ad88

Browse files
committed
Make NTRU's mod3 and modQ reductions division-free, restoring the reference implementation's fold-and-select and its MODQ mask so the secret operand is not exposed through division latency, with an exhaustive regression test and the Ant bctest include it needs; reported by the Robusta team.
1 parent 076ca36 commit 9c9ad88

5 files changed

Lines changed: 176 additions & 4 deletions

File tree

ant/bc+-build.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -988,6 +988,7 @@
988988
<fileset dir="${src.dir}" includes="**/crypto/*/*Test.java" />
989989
<fileset dir="${src.dir}" includes="**/crypto/agreement/owl/*Test.java" />
990990
<fileset dir="${src.dir}" includes="**/pqc/crypto/hqc/*Test.java" />
991+
<fileset dir="${src.dir}" includes="**/pqc/math/ntru/*Test.java" />
991992
<fileset dir="${src.dir}" includes="**/tsp/*UnitTest.java" />
992993
<fileset dir="${src.dir}" includes="**/utiltest/*Test.java" />
993994
<fileset dir="${src.dir}" includes="**/util/io/pem/*Test.java" />

core/src/main/java/org/bouncycastle/pqc/crypto/ntru/NTRUSampling.java

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -191,8 +191,20 @@ public HRSSPolynomial sampleIidPlus(byte[] uniformBytes)
191191
return r;
192192
}
193193

194+
// NOTE: this reduces the bytes of SHAKE(rmSeed) into the ternary message polynomials r and m
195+
// during encapsulation, and the secret key polynomials f and g during key generation, so it
196+
// must not divide. Same division-free fold-and-select as the reference mod3 (see
197+
// org.bouncycastle.pqc.math.ntru.Polynomial.mod3); do not replace it with a % 3.
194198
private static int mod3(int a)
195199
{
196-
return a % 3;
200+
int r = (a >>> 8) + (a & 0xff);
201+
r = (r >>> 4) + (r & 0x0f);
202+
r = (r >>> 2) + (r & 0x03);
203+
r = (r >>> 2) + (r & 0x03);
204+
205+
int t = r - 3;
206+
int c = t >> 31;
207+
208+
return (c & r) ^ (~c & t);
197209
}
198210
}

core/src/main/java/org/bouncycastle/pqc/math/ntru/Polynomial.java

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,21 +36,43 @@ static short bothNegativeMask(short x, short y)
3636
}
3737

3838
// defined in poly_mod.c
39+
//
40+
// NOTE: this runs on secret data - the secret key polynomials f and g during key
41+
// generation, the message polynomials r and m during encapsulation, and the recovered
42+
// coefficients during decapsulation - so it must not divide. Folding by 255, then 15, then
43+
// 3 preserves the residue mod 3 (3 divides both 255 and 15, and 4 = 1 mod 3) and leaves a
44+
// value of at most 5, which one masked conditional subtraction reduces to 0..2. This is the
45+
// reference implementation's division-free mod3; do not "simplify" it back to a % 3, which
46+
// compiles to an integer division whose latency depends on the secret operand.
3947
static short mod3(short a)
4048
{
41-
return (short)((a & 0xffff) % 3);
49+
int r = a & 0xffff;
50+
r = (r >>> 8) + (r & 0xff);
51+
r = (r >>> 4) + (r & 0x0f);
52+
r = (r >>> 2) + (r & 0x03);
53+
r = (r >>> 2) + (r & 0x03);
54+
55+
int t = r - 3;
56+
int c = t >> 31;
57+
58+
return (short)((c & r) ^ (~c & t));
4259
}
4360

4461
// defined in poly_s3_inv.c
4562
static byte mod3(byte a)
4663
{
47-
return (byte)((a & 0xff) % 3);
64+
return (byte)mod3((short)(a & 0xff));
4865
}
4966

5067
// defined in poly.h
68+
//
69+
// NOTE: q is always a power of two (NTRUParameterSet.q() returns 1 << logQ) and every call
70+
// site passes a value already masked to 16 bits, so masking is exactly x % q for the inputs
71+
// that occur here - and, unlike a division by a variable divisor, it cannot leak the secret
72+
// dividend through its latency. This matches the reference MODQ(X) ((X) & (NTRU_Q-1)).
5173
static int modQ(int x, int q)
5274
{
53-
return x % q;
75+
return x & (q - 1);
5476
}
5577

5678

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
package org.bouncycastle.pqc.math.ntru;
2+
3+
import junit.extensions.TestSetup;
4+
import junit.framework.Test;
5+
import junit.framework.TestCase;
6+
import junit.framework.TestSuite;
7+
import org.bouncycastle.test.PrintTestResult;
8+
9+
public class AllTests
10+
extends TestCase
11+
{
12+
public static void main(String[] args)
13+
{
14+
PrintTestResult.printResult(junit.textui.TestRunner.run(suite()));
15+
}
16+
17+
public static Test suite()
18+
{
19+
TestSuite suite = new TestSuite("NTRU Internal Tests");
20+
21+
suite.addTestSuite(PolynomialModTest.class);
22+
23+
return new BCTestSetup(suite);
24+
}
25+
26+
static class BCTestSetup
27+
extends TestSetup
28+
{
29+
public BCTestSetup(Test test)
30+
{
31+
super(test);
32+
}
33+
34+
protected void setUp()
35+
{
36+
}
37+
38+
protected void tearDown()
39+
{
40+
}
41+
}
42+
}
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
package org.bouncycastle.pqc.math.ntru;
2+
3+
import junit.framework.TestCase;
4+
5+
/**
6+
* Regression tests for NTRU's modular reductions.
7+
* <p>
8+
* {@link Polynomial#mod3(short)}, {@link Polynomial#mod3(byte)} and
9+
* {@link Polynomial#modQ(int, int)} used to be written with the {@code %} operator, which
10+
* compiles to an integer division whose latency depends on its operands - and all three run on
11+
* secret data (the secret key polynomials f and g, the message polynomials r and m, and the
12+
* coefficients recovered during decapsulation). They are now division-free, matching the
13+
* reference implementation, and these tests pin them to the values {@code %} produced across the
14+
* entire input domain that reaches them.
15+
* <p>
16+
* This test has to live in {@code org.bouncycastle.pqc.math.ntru} rather than the
17+
* {@code ...ntru.test} package alongside {@link org.bouncycastle.pqc.math.ntru.test.PolynomialTest},
18+
* because the methods are package-private.
19+
*/
20+
public class PolynomialModTest
21+
extends TestCase
22+
{
23+
/**
24+
* Every logQ appearing in a parameter set: NTRUHPS2048509/677 use 11, NTRUHPS4096821/1229
25+
* use 12, NTRUHRSS701 uses 13 and NTRUHRSS1373 uses 14. NTRUParameterSet.q() is 1 << logQ,
26+
* which is what makes masking a valid reduction.
27+
*/
28+
private static final int[] LOG_QS = new int[]{11, 12, 13, 14};
29+
30+
public void testMod3ShortOverEveryInput()
31+
{
32+
for (int a = Short.MIN_VALUE; a <= Short.MAX_VALUE; ++a)
33+
{
34+
short s = (short)a;
35+
assertEquals("mod3(short " + s + ")", (s & 0xffff) % 3, Polynomial.mod3(s));
36+
}
37+
}
38+
39+
public void testMod3ByteOverEveryInput()
40+
{
41+
for (int a = Byte.MIN_VALUE; a <= Byte.MAX_VALUE; ++a)
42+
{
43+
byte b = (byte)a;
44+
assertEquals("mod3(byte " + b + ")", (byte)((b & 0xff) % 3), Polynomial.mod3(b));
45+
}
46+
}
47+
48+
/**
49+
* The callers treat the result as a ternary coefficient, so nothing may escape 0..2 - the
50+
* masked conditional subtraction has to leave the folded value fully reduced.
51+
*/
52+
public void testMod3IsAlwaysTernary()
53+
{
54+
for (int a = Short.MIN_VALUE; a <= Short.MAX_VALUE; ++a)
55+
{
56+
short r = Polynomial.mod3((short)a);
57+
assertTrue("mod3(short " + a + ") = " + r, r >= 0 && r <= 2);
58+
}
59+
60+
for (int a = Byte.MIN_VALUE; a <= Byte.MAX_VALUE; ++a)
61+
{
62+
byte r = Polynomial.mod3((byte)a);
63+
assertTrue("mod3(byte " + a + ") = " + r, r >= 0 && r <= 2);
64+
}
65+
}
66+
67+
/**
68+
* Every call site passes a dividend already masked to 16 bits, so that is the domain that
69+
* has to agree with {@code %}; q is a power of two for every parameter set.
70+
*/
71+
public void testModQOverEveryInputAndParameterSet()
72+
{
73+
for (int i = 0; i < LOG_QS.length; ++i)
74+
{
75+
int q = 1 << LOG_QS[i];
76+
for (int x = 0; x <= 0xffff; ++x)
77+
{
78+
assertEquals("modQ(" + x + ", " + q + ")", x % q, Polynomial.modQ(x, q));
79+
}
80+
}
81+
}
82+
83+
public void testModQIsInRange()
84+
{
85+
for (int i = 0; i < LOG_QS.length; ++i)
86+
{
87+
int q = 1 << LOG_QS[i];
88+
for (int x = 0; x <= 0xffff; ++x)
89+
{
90+
int r = Polynomial.modQ(x, q);
91+
assertTrue("modQ(" + x + ", " + q + ") = " + r, r >= 0 && r < q);
92+
}
93+
}
94+
}
95+
}

0 commit comments

Comments
 (0)