Skip to content

Commit cc6ba6c

Browse files
committed
ML-DSA: Run the Project Wycheproof verify vectors (631 cases, 404 of them invalid by construction) against the lightweight signer, with a minimal JSON reader as nothing in the tree provided one
1 parent 4c78802 commit cc6ba6c

3 files changed

Lines changed: 530 additions & 0 deletions

File tree

‎core/src/test/java/org/bouncycastle/pqc/crypto/test/AllTests.java‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ public static Test suite()
3636
suite.addTestSuite(MLKEMTest.class);
3737
suite.addTestSuite(CrystalsDilithiumTest.class);
3838
suite.addTestSuite(MLDSATest.class);
39+
suite.addTestSuite(MLDSAWycheproofTest.class);
3940
suite.addTestSuite(NTRULPRimeTest.class);
4041
suite.addTestSuite(SNTRUPrimeTest.class);
4142
// suite.addTestSuite(BIKETest.class);
Lines changed: 316 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,316 @@
1+
package org.bouncycastle.pqc.crypto.test;
2+
3+
import java.io.IOException;
4+
import java.io.InputStream;
5+
import java.io.InputStreamReader;
6+
import java.io.Reader;
7+
import java.util.ArrayList;
8+
import java.util.LinkedHashMap;
9+
import java.util.List;
10+
import java.util.Map;
11+
12+
/**
13+
* Minimal recursive-descent JSON reader for the test vector files, which no other dependency here
14+
* provides. Objects come back as Map, arrays as List, strings as String, numbers as Integer or
15+
* Double, and the three literals as Boolean or null. Not a general purpose parser: it is here to
16+
* read data files of a known shape, so it favours rejecting anything it does not understand over
17+
* accepting the wider grammar.
18+
*/
19+
class JsonParser
20+
{
21+
static Map parseObject(InputStream in)
22+
throws IOException
23+
{
24+
Reader reader = new InputStreamReader(in, "UTF-8");
25+
StringBuffer buf = new StringBuffer();
26+
char[] block = new char[8192];
27+
int len;
28+
29+
while ((len = reader.read(block)) >= 0)
30+
{
31+
buf.append(block, 0, len);
32+
}
33+
34+
JsonParser parser = new JsonParser(buf.toString());
35+
Object value = parser.readValue();
36+
37+
parser.skipWhitespace();
38+
if (parser.pos != parser.json.length())
39+
{
40+
throw new IOException("trailing content after JSON value at offset " + parser.pos);
41+
}
42+
if (!(value instanceof Map))
43+
{
44+
throw new IOException("expected a JSON object at the top level");
45+
}
46+
47+
return (Map)value;
48+
}
49+
50+
private final String json;
51+
private int pos;
52+
53+
private JsonParser(String json)
54+
{
55+
this.json = json;
56+
}
57+
58+
private Object readValue()
59+
throws IOException
60+
{
61+
skipWhitespace();
62+
63+
char c = peek();
64+
switch (c)
65+
{
66+
case '{':
67+
return readObject();
68+
case '[':
69+
return readArray();
70+
case '"':
71+
return readString();
72+
case 't':
73+
expect("true");
74+
return Boolean.TRUE;
75+
case 'f':
76+
expect("false");
77+
return Boolean.FALSE;
78+
case 'n':
79+
expect("null");
80+
return null;
81+
default:
82+
return readNumber();
83+
}
84+
}
85+
86+
private Map readObject()
87+
throws IOException
88+
{
89+
Map map = new LinkedHashMap();
90+
91+
pos++;
92+
skipWhitespace();
93+
94+
if (peek() == '}')
95+
{
96+
pos++;
97+
return map;
98+
}
99+
100+
for (; ; )
101+
{
102+
skipWhitespace();
103+
104+
String name = readString();
105+
106+
skipWhitespace();
107+
if (next() != ':')
108+
{
109+
throw parseError("expected ':' after a member name");
110+
}
111+
112+
map.put(name, readValue());
113+
114+
skipWhitespace();
115+
char c = next();
116+
if (c == '}')
117+
{
118+
return map;
119+
}
120+
if (c != ',')
121+
{
122+
throw parseError("expected ',' or '}' in an object");
123+
}
124+
}
125+
}
126+
127+
private List readArray()
128+
throws IOException
129+
{
130+
List list = new ArrayList();
131+
132+
pos++;
133+
skipWhitespace();
134+
135+
if (peek() == ']')
136+
{
137+
pos++;
138+
return list;
139+
}
140+
141+
for (; ; )
142+
{
143+
list.add(readValue());
144+
145+
skipWhitespace();
146+
char c = next();
147+
if (c == ']')
148+
{
149+
return list;
150+
}
151+
if (c != ',')
152+
{
153+
throw parseError("expected ',' or ']' in an array");
154+
}
155+
}
156+
}
157+
158+
private String readString()
159+
throws IOException
160+
{
161+
if (next() != '"')
162+
{
163+
throw parseError("expected a string");
164+
}
165+
166+
StringBuffer buf = new StringBuffer();
167+
168+
for (; ; )
169+
{
170+
char c = next();
171+
172+
if (c == '"')
173+
{
174+
return buf.toString();
175+
}
176+
177+
if (c != '\\')
178+
{
179+
if (c < ' ')
180+
{
181+
throw parseError("unescaped control character in a string");
182+
}
183+
buf.append(c);
184+
continue;
185+
}
186+
187+
char esc = next();
188+
switch (esc)
189+
{
190+
case '"':
191+
case '\\':
192+
case '/':
193+
buf.append(esc);
194+
break;
195+
case 'b':
196+
buf.append('\b');
197+
break;
198+
case 'f':
199+
buf.append('\f');
200+
break;
201+
case 'n':
202+
buf.append('\n');
203+
break;
204+
case 'r':
205+
buf.append('\r');
206+
break;
207+
case 't':
208+
buf.append('\t');
209+
break;
210+
case 'u':
211+
if (pos + 4 > json.length())
212+
{
213+
throw parseError("truncated \\u escape");
214+
}
215+
buf.append((char)Integer.parseInt(json.substring(pos, pos + 4), 16));
216+
pos += 4;
217+
break;
218+
default:
219+
throw parseError("unrecognised escape '\\" + esc + "'");
220+
}
221+
}
222+
}
223+
224+
private Object readNumber()
225+
throws IOException
226+
{
227+
int start = pos;
228+
boolean isInteger = true;
229+
230+
if (peek() == '-')
231+
{
232+
pos++;
233+
}
234+
235+
while (pos != json.length())
236+
{
237+
char c = json.charAt(pos);
238+
if (c >= '0' && c <= '9')
239+
{
240+
pos++;
241+
}
242+
else if (c == '.' || c == 'e' || c == 'E' || c == '+' || c == '-')
243+
{
244+
isInteger = false;
245+
pos++;
246+
}
247+
else
248+
{
249+
break;
250+
}
251+
}
252+
253+
if (pos == start)
254+
{
255+
throw parseError("expected a value");
256+
}
257+
258+
String number = json.substring(start, pos);
259+
try
260+
{
261+
// the vector files only carry test case numbers, so an int is enough for those; keep a
262+
// double for anything else rather than failing on a file that grows a real number.
263+
return isInteger ? (Object)Integer.valueOf(number) : (Object)Double.valueOf(number);
264+
}
265+
catch (NumberFormatException e)
266+
{
267+
throw parseError("malformed number '" + number + "'");
268+
}
269+
}
270+
271+
private void expect(String literal)
272+
throws IOException
273+
{
274+
if (!json.startsWith(literal, pos))
275+
{
276+
throw parseError("expected '" + literal + "'");
277+
}
278+
pos += literal.length();
279+
}
280+
281+
private void skipWhitespace()
282+
{
283+
while (pos != json.length())
284+
{
285+
char c = json.charAt(pos);
286+
if (c != ' ' && c != '\t' && c != '\n' && c != '\r')
287+
{
288+
return;
289+
}
290+
pos++;
291+
}
292+
}
293+
294+
private char peek()
295+
throws IOException
296+
{
297+
if (pos == json.length())
298+
{
299+
throw parseError("unexpected end of input");
300+
}
301+
return json.charAt(pos);
302+
}
303+
304+
private char next()
305+
throws IOException
306+
{
307+
char c = peek();
308+
pos++;
309+
return c;
310+
}
311+
312+
private IOException parseError(String message)
313+
{
314+
return new IOException(message + " at offset " + pos);
315+
}
316+
}

0 commit comments

Comments
 (0)