@theqrl/mldsa87 1.1.0 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +19 -17
- package/dist/cjs/mldsa87.js +421 -44
- package/dist/mjs/mldsa87.js +30 -30
- package/package.json +34 -14
- package/src/index.d.ts +10 -10
package/README.md
CHANGED
|
@@ -26,12 +26,13 @@ const pk = new Uint8Array(CryptoPublicKeyBytes); // 2592 bytes
|
|
|
26
26
|
const sk = new Uint8Array(CryptoSecretKeyBytes); // 4896 bytes
|
|
27
27
|
cryptoSignKeypair(null, pk, sk); // null = random seed
|
|
28
28
|
|
|
29
|
-
// Sign a message
|
|
29
|
+
// Sign a message
|
|
30
30
|
const message = new TextEncoder().encode('Hello, quantum world!');
|
|
31
|
-
const
|
|
31
|
+
const ctx = new Uint8Array([0x5a, 0x4f, 0x4e, 0x44]); // "ZOND"
|
|
32
|
+
const signedMessage = cryptoSign(message, sk, false, ctx); // false = deterministic
|
|
32
33
|
|
|
33
|
-
// Verify and extract
|
|
34
|
-
const extracted = cryptoSignOpen(signedMessage, pk);
|
|
34
|
+
// Verify and extract (context must match)
|
|
35
|
+
const extracted = cryptoSignOpen(signedMessage, pk, ctx);
|
|
35
36
|
if (extracted === undefined) {
|
|
36
37
|
throw new Error('Invalid signature');
|
|
37
38
|
}
|
|
@@ -40,20 +41,21 @@ console.log(new TextDecoder().decode(extracted)); // "Hello, quantum world!"
|
|
|
40
41
|
|
|
41
42
|
## Context Parameter
|
|
42
43
|
|
|
43
|
-
ML-DSA-87
|
|
44
|
+
ML-DSA-87 requires a context parameter for domain separation (FIPS 204 feature). This allows the same keypair to be used safely across different applications.
|
|
44
45
|
|
|
45
46
|
```javascript
|
|
46
|
-
// With
|
|
47
|
+
// With application-specific context
|
|
47
48
|
const ctx = new TextEncoder().encode('my-app-v1');
|
|
48
49
|
const signed = cryptoSign(message, sk, false, ctx);
|
|
49
50
|
const extracted = cryptoSignOpen(signed, pk, ctx);
|
|
50
51
|
|
|
51
52
|
// Context must match for verification
|
|
52
|
-
|
|
53
|
-
cryptoSignOpen(signed, pk,
|
|
53
|
+
const wrongCtx = new Uint8Array(0);
|
|
54
|
+
cryptoSignOpen(signed, pk, wrongCtx); // undefined - wrong context
|
|
55
|
+
cryptoSignOpen(signed, pk, ctx); // message - correct context
|
|
54
56
|
```
|
|
55
57
|
|
|
56
|
-
|
|
58
|
+
Context is a required `Uint8Array` and can be 0-255 bytes. Use an empty `Uint8Array(0)` if no domain separation is needed.
|
|
57
59
|
|
|
58
60
|
## API
|
|
59
61
|
|
|
@@ -77,26 +79,26 @@ Generate a keypair from a seed.
|
|
|
77
79
|
- `sk`: `Uint8Array(4896)` - output buffer for secret key
|
|
78
80
|
- Returns: The seed used (useful when `seed` is `null`)
|
|
79
81
|
|
|
80
|
-
#### `cryptoSign(message, sk, randomized, context
|
|
82
|
+
#### `cryptoSign(message, sk, randomized, context)`
|
|
81
83
|
|
|
82
84
|
Sign a message (combined mode: returns signature || message).
|
|
83
85
|
|
|
84
86
|
- `message`: `Uint8Array` or `string` - message bytes; if `string`, it must be hex only (optional `0x`, even length). Plain-text strings are not accepted.
|
|
85
87
|
- `sk`: `Uint8Array(4896)` - secret key
|
|
86
88
|
- `randomized`: `boolean` - `true` for hedged signing, `false` for deterministic
|
|
87
|
-
- `context`: `Uint8Array`
|
|
89
|
+
- `context`: `Uint8Array` - context string for domain separation, 0-255 bytes
|
|
88
90
|
- Returns: `Uint8Array` containing signature + message
|
|
89
91
|
|
|
90
|
-
#### `cryptoSignOpen(signedMessage, pk, context
|
|
92
|
+
#### `cryptoSignOpen(signedMessage, pk, context)`
|
|
91
93
|
|
|
92
94
|
Verify and extract message from signed message.
|
|
93
95
|
|
|
94
96
|
- `signedMessage`: `Uint8Array` - output from `cryptoSign()`
|
|
95
97
|
- `pk`: `Uint8Array(2592)` - public key
|
|
96
|
-
- `context`: `Uint8Array`
|
|
98
|
+
- `context`: `Uint8Array` - must match signing context
|
|
97
99
|
- Returns: Original message if valid, `undefined` if verification fails
|
|
98
100
|
|
|
99
|
-
#### `cryptoSignSignature(sig, message, sk, randomized, context
|
|
101
|
+
#### `cryptoSignSignature(sig, message, sk, randomized, context)`
|
|
100
102
|
|
|
101
103
|
Create a detached signature.
|
|
102
104
|
|
|
@@ -104,17 +106,17 @@ Create a detached signature.
|
|
|
104
106
|
- `message`: `Uint8Array` or `string` - message bytes; if `string`, it must be hex only (optional `0x`, even length). Plain-text strings are not accepted.
|
|
105
107
|
- `sk`: `Uint8Array(4896)` - secret key
|
|
106
108
|
- `randomized`: `boolean` - `true` for hedged, `false` for deterministic
|
|
107
|
-
- `context`: `Uint8Array`
|
|
109
|
+
- `context`: `Uint8Array` - context string for domain separation, 0-255 bytes
|
|
108
110
|
- Returns: `0` on success
|
|
109
111
|
|
|
110
|
-
#### `cryptoSignVerify(sig, message, pk, context
|
|
112
|
+
#### `cryptoSignVerify(sig, message, pk, context)`
|
|
111
113
|
|
|
112
114
|
Verify a detached signature.
|
|
113
115
|
|
|
114
116
|
- `sig`: `Uint8Array(4627)` - signature to verify
|
|
115
117
|
- `message`: `Uint8Array` or `string` - original message bytes; if `string`, it must be hex only (optional `0x`, even length). Plain-text strings are not accepted.
|
|
116
118
|
- `pk`: `Uint8Array(2592)` - public key
|
|
117
|
-
- `context`: `Uint8Array`
|
|
119
|
+
- `context`: `Uint8Array` - must match signing context
|
|
118
120
|
- Returns: `true` if valid, `false` otherwise
|
|
119
121
|
|
|
120
122
|
**Note:** To sign or verify plain text, convert it to bytes (e.g., `new TextEncoder().encode('Hello')`). String inputs are interpreted as hex only.
|
package/dist/cjs/mldsa87.js
CHANGED
|
@@ -1,8 +1,5 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
var sha3_js = require('@noble/hashes/sha3.js');
|
|
4
|
-
var utils_js = require('@noble/hashes/utils.js');
|
|
5
|
-
|
|
6
3
|
const Shake128Rate = 168;
|
|
7
4
|
const Shake256Rate = 136;
|
|
8
5
|
const Stream128BlockBytes = Shake128Rate;
|
|
@@ -67,6 +64,386 @@ const zetas = [
|
|
|
67
64
|
-1362209, 3937738, 1400424, -846154, 1976782,
|
|
68
65
|
];
|
|
69
66
|
|
|
67
|
+
/**
|
|
68
|
+
* Internal helpers for u64. BigUint64Array is too slow as per 2025, so we implement it using Uint32Array.
|
|
69
|
+
* @todo re-check https://issues.chromium.org/issues/42212588
|
|
70
|
+
* @module
|
|
71
|
+
*/
|
|
72
|
+
const U32_MASK64 = /* @__PURE__ */ BigInt(2 ** 32 - 1);
|
|
73
|
+
const _32n = /* @__PURE__ */ BigInt(32);
|
|
74
|
+
function fromBig(n, le = false) {
|
|
75
|
+
if (le)
|
|
76
|
+
return { h: Number(n & U32_MASK64), l: Number((n >> _32n) & U32_MASK64) };
|
|
77
|
+
return { h: Number((n >> _32n) & U32_MASK64) | 0, l: Number(n & U32_MASK64) | 0 };
|
|
78
|
+
}
|
|
79
|
+
function split(lst, le = false) {
|
|
80
|
+
const len = lst.length;
|
|
81
|
+
let Ah = new Uint32Array(len);
|
|
82
|
+
let Al = new Uint32Array(len);
|
|
83
|
+
for (let i = 0; i < len; i++) {
|
|
84
|
+
const { h, l } = fromBig(lst[i], le);
|
|
85
|
+
[Ah[i], Al[i]] = [h, l];
|
|
86
|
+
}
|
|
87
|
+
return [Ah, Al];
|
|
88
|
+
}
|
|
89
|
+
// Left rotate for Shift in [1, 32)
|
|
90
|
+
const rotlSH = (h, l, s) => (h << s) | (l >>> (32 - s));
|
|
91
|
+
const rotlSL = (h, l, s) => (l << s) | (h >>> (32 - s));
|
|
92
|
+
// Left rotate for Shift in (32, 64), NOTE: 32 is special case.
|
|
93
|
+
const rotlBH = (h, l, s) => (l << (s - 32)) | (h >>> (64 - s));
|
|
94
|
+
const rotlBL = (h, l, s) => (h << (s - 32)) | (l >>> (64 - s));
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Utilities for hex, bytes, CSPRNG.
|
|
98
|
+
* @module
|
|
99
|
+
*/
|
|
100
|
+
/*! noble-hashes - MIT License (c) 2022 Paul Miller (paulmillr.com) */
|
|
101
|
+
/** Checks if something is Uint8Array. Be careful: nodejs Buffer will return true. */
|
|
102
|
+
function isBytes(a) {
|
|
103
|
+
return a instanceof Uint8Array || (ArrayBuffer.isView(a) && a.constructor.name === 'Uint8Array');
|
|
104
|
+
}
|
|
105
|
+
/** Asserts something is positive integer. */
|
|
106
|
+
function anumber(n, title = '') {
|
|
107
|
+
if (!Number.isSafeInteger(n) || n < 0) {
|
|
108
|
+
const prefix = title && `"${title}" `;
|
|
109
|
+
throw new Error(`${prefix}expected integer >= 0, got ${n}`);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
/** Asserts something is Uint8Array. */
|
|
113
|
+
function abytes(value, length, title = '') {
|
|
114
|
+
const bytes = isBytes(value);
|
|
115
|
+
const len = value?.length;
|
|
116
|
+
const needsLen = length !== undefined;
|
|
117
|
+
if (!bytes || (needsLen)) {
|
|
118
|
+
const prefix = title && `"${title}" `;
|
|
119
|
+
const ofLen = '';
|
|
120
|
+
const got = bytes ? `length=${len}` : `type=${typeof value}`;
|
|
121
|
+
throw new Error(prefix + 'expected Uint8Array' + ofLen + ', got ' + got);
|
|
122
|
+
}
|
|
123
|
+
return value;
|
|
124
|
+
}
|
|
125
|
+
/** Asserts a hash instance has not been destroyed / finished */
|
|
126
|
+
function aexists(instance, checkFinished = true) {
|
|
127
|
+
if (instance.destroyed)
|
|
128
|
+
throw new Error('Hash instance has been destroyed');
|
|
129
|
+
if (checkFinished && instance.finished)
|
|
130
|
+
throw new Error('Hash#digest() has already been called');
|
|
131
|
+
}
|
|
132
|
+
/** Asserts output is properly-sized byte array */
|
|
133
|
+
function aoutput(out, instance) {
|
|
134
|
+
abytes(out, undefined, 'digestInto() output');
|
|
135
|
+
const min = instance.outputLen;
|
|
136
|
+
if (out.length < min) {
|
|
137
|
+
throw new Error('"digestInto() output" expected to be of length >=' + min);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
/** Cast u8 / u16 / u32 to u32. */
|
|
141
|
+
function u32(arr) {
|
|
142
|
+
return new Uint32Array(arr.buffer, arr.byteOffset, Math.floor(arr.byteLength / 4));
|
|
143
|
+
}
|
|
144
|
+
/** Zeroize a byte array. Warning: JS provides no guarantees. */
|
|
145
|
+
function clean(...arrays) {
|
|
146
|
+
for (let i = 0; i < arrays.length; i++) {
|
|
147
|
+
arrays[i].fill(0);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
/** Is current platform little-endian? Most are. Big-Endian platform: IBM */
|
|
151
|
+
const isLE = /* @__PURE__ */ (() => new Uint8Array(new Uint32Array([0x11223344]).buffer)[0] === 0x44)();
|
|
152
|
+
/** The byte swap operation for uint32 */
|
|
153
|
+
function byteSwap(word) {
|
|
154
|
+
return (((word << 24) & 0xff000000) |
|
|
155
|
+
((word << 8) & 0xff0000) |
|
|
156
|
+
((word >>> 8) & 0xff00) |
|
|
157
|
+
((word >>> 24) & 0xff));
|
|
158
|
+
}
|
|
159
|
+
/** In place byte swap for Uint32Array */
|
|
160
|
+
function byteSwap32(arr) {
|
|
161
|
+
for (let i = 0; i < arr.length; i++) {
|
|
162
|
+
arr[i] = byteSwap(arr[i]);
|
|
163
|
+
}
|
|
164
|
+
return arr;
|
|
165
|
+
}
|
|
166
|
+
const swap32IfBE = isLE
|
|
167
|
+
? (u) => u
|
|
168
|
+
: byteSwap32;
|
|
169
|
+
// Built-in hex conversion https://caniuse.com/mdn-javascript_builtins_uint8array_fromhex
|
|
170
|
+
const hasHexBuiltin = /* @__PURE__ */ (() =>
|
|
171
|
+
// @ts-ignore
|
|
172
|
+
typeof Uint8Array.from([]).toHex === 'function' && typeof Uint8Array.fromHex === 'function')();
|
|
173
|
+
// We use optimized technique to convert hex string to byte array
|
|
174
|
+
const asciis = { _0: 48, _9: 57, A: 65, F: 70, a: 97, f: 102 };
|
|
175
|
+
function asciiToBase16(ch) {
|
|
176
|
+
if (ch >= asciis._0 && ch <= asciis._9)
|
|
177
|
+
return ch - asciis._0; // '2' => 50-48
|
|
178
|
+
if (ch >= asciis.A && ch <= asciis.F)
|
|
179
|
+
return ch - (asciis.A - 10); // 'B' => 66-(65-10)
|
|
180
|
+
if (ch >= asciis.a && ch <= asciis.f)
|
|
181
|
+
return ch - (asciis.a - 10); // 'b' => 98-(97-10)
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* Convert hex string to byte array. Uses built-in function, when available.
|
|
186
|
+
* @example hexToBytes('cafe0123') // Uint8Array.from([0xca, 0xfe, 0x01, 0x23])
|
|
187
|
+
*/
|
|
188
|
+
function hexToBytes$1(hex) {
|
|
189
|
+
if (typeof hex !== 'string')
|
|
190
|
+
throw new Error('hex string expected, got ' + typeof hex);
|
|
191
|
+
// @ts-ignore
|
|
192
|
+
if (hasHexBuiltin)
|
|
193
|
+
return Uint8Array.fromHex(hex);
|
|
194
|
+
const hl = hex.length;
|
|
195
|
+
const al = hl / 2;
|
|
196
|
+
if (hl % 2)
|
|
197
|
+
throw new Error('hex string expected, got unpadded hex of length ' + hl);
|
|
198
|
+
const array = new Uint8Array(al);
|
|
199
|
+
for (let ai = 0, hi = 0; ai < al; ai++, hi += 2) {
|
|
200
|
+
const n1 = asciiToBase16(hex.charCodeAt(hi));
|
|
201
|
+
const n2 = asciiToBase16(hex.charCodeAt(hi + 1));
|
|
202
|
+
if (n1 === undefined || n2 === undefined) {
|
|
203
|
+
const char = hex[hi] + hex[hi + 1];
|
|
204
|
+
throw new Error('hex string expected, got non-hex character "' + char + '" at index ' + hi);
|
|
205
|
+
}
|
|
206
|
+
array[ai] = n1 * 16 + n2; // multiply first octet, e.g. 'a3' => 10*16+3 => 160 + 3 => 163
|
|
207
|
+
}
|
|
208
|
+
return array;
|
|
209
|
+
}
|
|
210
|
+
/** Creates function with outputLen, blockLen, create properties from a class constructor. */
|
|
211
|
+
function createHasher(hashCons, info = {}) {
|
|
212
|
+
const hashC = (msg, opts) => hashCons(opts).update(msg).digest();
|
|
213
|
+
const tmp = hashCons(undefined);
|
|
214
|
+
hashC.outputLen = tmp.outputLen;
|
|
215
|
+
hashC.blockLen = tmp.blockLen;
|
|
216
|
+
hashC.create = (opts) => hashCons(opts);
|
|
217
|
+
Object.assign(hashC, info);
|
|
218
|
+
return Object.freeze(hashC);
|
|
219
|
+
}
|
|
220
|
+
/** Creates OID opts for NIST hashes, with prefix 06 09 60 86 48 01 65 03 04 02. */
|
|
221
|
+
const oidNist = (suffix) => ({
|
|
222
|
+
oid: Uint8Array.from([0x06, 0x09, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, suffix]),
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* SHA3 (keccak) hash function, based on a new "Sponge function" design.
|
|
227
|
+
* Different from older hashes, the internal state is bigger than output size.
|
|
228
|
+
*
|
|
229
|
+
* Check out [FIPS-202](https://nvlpubs.nist.gov/nistpubs/FIPS/NIST.FIPS.202.pdf),
|
|
230
|
+
* [Website](https://keccak.team/keccak.html),
|
|
231
|
+
* [the differences between SHA-3 and Keccak](https://crypto.stackexchange.com/questions/15727/what-are-the-key-differences-between-the-draft-sha-3-standard-and-the-keccak-sub).
|
|
232
|
+
*
|
|
233
|
+
* Check out `sha3-addons` module for cSHAKE, k12, and others.
|
|
234
|
+
* @module
|
|
235
|
+
*/
|
|
236
|
+
// No __PURE__ annotations in sha3 header:
|
|
237
|
+
// EVERYTHING is in fact used on every export.
|
|
238
|
+
// Various per round constants calculations
|
|
239
|
+
const _0n = BigInt(0);
|
|
240
|
+
const _1n = BigInt(1);
|
|
241
|
+
const _2n = BigInt(2);
|
|
242
|
+
const _7n = BigInt(7);
|
|
243
|
+
const _256n = BigInt(256);
|
|
244
|
+
const _0x71n = BigInt(0x71);
|
|
245
|
+
const SHA3_PI = [];
|
|
246
|
+
const SHA3_ROTL = [];
|
|
247
|
+
const _SHA3_IOTA = []; // no pure annotation: var is always used
|
|
248
|
+
for (let round = 0, R = _1n, x = 1, y = 0; round < 24; round++) {
|
|
249
|
+
// Pi
|
|
250
|
+
[x, y] = [y, (2 * x + 3 * y) % 5];
|
|
251
|
+
SHA3_PI.push(2 * (5 * y + x));
|
|
252
|
+
// Rotational
|
|
253
|
+
SHA3_ROTL.push((((round + 1) * (round + 2)) / 2) % 64);
|
|
254
|
+
// Iota
|
|
255
|
+
let t = _0n;
|
|
256
|
+
for (let j = 0; j < 7; j++) {
|
|
257
|
+
R = ((R << _1n) ^ ((R >> _7n) * _0x71n)) % _256n;
|
|
258
|
+
if (R & _2n)
|
|
259
|
+
t ^= _1n << ((_1n << BigInt(j)) - _1n);
|
|
260
|
+
}
|
|
261
|
+
_SHA3_IOTA.push(t);
|
|
262
|
+
}
|
|
263
|
+
const IOTAS = split(_SHA3_IOTA, true);
|
|
264
|
+
const SHA3_IOTA_H = IOTAS[0];
|
|
265
|
+
const SHA3_IOTA_L = IOTAS[1];
|
|
266
|
+
// Left rotation (without 0, 32, 64)
|
|
267
|
+
const rotlH = (h, l, s) => (s > 32 ? rotlBH(h, l, s) : rotlSH(h, l, s));
|
|
268
|
+
const rotlL = (h, l, s) => (s > 32 ? rotlBL(h, l, s) : rotlSL(h, l, s));
|
|
269
|
+
/** `keccakf1600` internal function, additionally allows to adjust round count. */
|
|
270
|
+
function keccakP(s, rounds = 24) {
|
|
271
|
+
const B = new Uint32Array(5 * 2);
|
|
272
|
+
// NOTE: all indices are x2 since we store state as u32 instead of u64 (bigints to slow in js)
|
|
273
|
+
for (let round = 24 - rounds; round < 24; round++) {
|
|
274
|
+
// Theta θ
|
|
275
|
+
for (let x = 0; x < 10; x++)
|
|
276
|
+
B[x] = s[x] ^ s[x + 10] ^ s[x + 20] ^ s[x + 30] ^ s[x + 40];
|
|
277
|
+
for (let x = 0; x < 10; x += 2) {
|
|
278
|
+
const idx1 = (x + 8) % 10;
|
|
279
|
+
const idx0 = (x + 2) % 10;
|
|
280
|
+
const B0 = B[idx0];
|
|
281
|
+
const B1 = B[idx0 + 1];
|
|
282
|
+
const Th = rotlH(B0, B1, 1) ^ B[idx1];
|
|
283
|
+
const Tl = rotlL(B0, B1, 1) ^ B[idx1 + 1];
|
|
284
|
+
for (let y = 0; y < 50; y += 10) {
|
|
285
|
+
s[x + y] ^= Th;
|
|
286
|
+
s[x + y + 1] ^= Tl;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
// Rho (ρ) and Pi (π)
|
|
290
|
+
let curH = s[2];
|
|
291
|
+
let curL = s[3];
|
|
292
|
+
for (let t = 0; t < 24; t++) {
|
|
293
|
+
const shift = SHA3_ROTL[t];
|
|
294
|
+
const Th = rotlH(curH, curL, shift);
|
|
295
|
+
const Tl = rotlL(curH, curL, shift);
|
|
296
|
+
const PI = SHA3_PI[t];
|
|
297
|
+
curH = s[PI];
|
|
298
|
+
curL = s[PI + 1];
|
|
299
|
+
s[PI] = Th;
|
|
300
|
+
s[PI + 1] = Tl;
|
|
301
|
+
}
|
|
302
|
+
// Chi (χ)
|
|
303
|
+
for (let y = 0; y < 50; y += 10) {
|
|
304
|
+
for (let x = 0; x < 10; x++)
|
|
305
|
+
B[x] = s[y + x];
|
|
306
|
+
for (let x = 0; x < 10; x++)
|
|
307
|
+
s[y + x] ^= ~B[(x + 2) % 10] & B[(x + 4) % 10];
|
|
308
|
+
}
|
|
309
|
+
// Iota (ι)
|
|
310
|
+
s[0] ^= SHA3_IOTA_H[round];
|
|
311
|
+
s[1] ^= SHA3_IOTA_L[round];
|
|
312
|
+
}
|
|
313
|
+
clean(B);
|
|
314
|
+
}
|
|
315
|
+
/** Keccak sponge function. */
|
|
316
|
+
class Keccak {
|
|
317
|
+
state;
|
|
318
|
+
pos = 0;
|
|
319
|
+
posOut = 0;
|
|
320
|
+
finished = false;
|
|
321
|
+
state32;
|
|
322
|
+
destroyed = false;
|
|
323
|
+
blockLen;
|
|
324
|
+
suffix;
|
|
325
|
+
outputLen;
|
|
326
|
+
enableXOF = false;
|
|
327
|
+
rounds;
|
|
328
|
+
// NOTE: we accept arguments in bytes instead of bits here.
|
|
329
|
+
constructor(blockLen, suffix, outputLen, enableXOF = false, rounds = 24) {
|
|
330
|
+
this.blockLen = blockLen;
|
|
331
|
+
this.suffix = suffix;
|
|
332
|
+
this.outputLen = outputLen;
|
|
333
|
+
this.enableXOF = enableXOF;
|
|
334
|
+
this.rounds = rounds;
|
|
335
|
+
// Can be passed from user as dkLen
|
|
336
|
+
anumber(outputLen, 'outputLen');
|
|
337
|
+
// 1600 = 5x5 matrix of 64bit. 1600 bits === 200 bytes
|
|
338
|
+
// 0 < blockLen < 200
|
|
339
|
+
if (!(0 < blockLen && blockLen < 200))
|
|
340
|
+
throw new Error('only keccak-f1600 function is supported');
|
|
341
|
+
this.state = new Uint8Array(200);
|
|
342
|
+
this.state32 = u32(this.state);
|
|
343
|
+
}
|
|
344
|
+
clone() {
|
|
345
|
+
return this._cloneInto();
|
|
346
|
+
}
|
|
347
|
+
keccak() {
|
|
348
|
+
swap32IfBE(this.state32);
|
|
349
|
+
keccakP(this.state32, this.rounds);
|
|
350
|
+
swap32IfBE(this.state32);
|
|
351
|
+
this.posOut = 0;
|
|
352
|
+
this.pos = 0;
|
|
353
|
+
}
|
|
354
|
+
update(data) {
|
|
355
|
+
aexists(this);
|
|
356
|
+
abytes(data);
|
|
357
|
+
const { blockLen, state } = this;
|
|
358
|
+
const len = data.length;
|
|
359
|
+
for (let pos = 0; pos < len;) {
|
|
360
|
+
const take = Math.min(blockLen - this.pos, len - pos);
|
|
361
|
+
for (let i = 0; i < take; i++)
|
|
362
|
+
state[this.pos++] ^= data[pos++];
|
|
363
|
+
if (this.pos === blockLen)
|
|
364
|
+
this.keccak();
|
|
365
|
+
}
|
|
366
|
+
return this;
|
|
367
|
+
}
|
|
368
|
+
finish() {
|
|
369
|
+
if (this.finished)
|
|
370
|
+
return;
|
|
371
|
+
this.finished = true;
|
|
372
|
+
const { state, suffix, pos, blockLen } = this;
|
|
373
|
+
// Do the padding
|
|
374
|
+
state[pos] ^= suffix;
|
|
375
|
+
if ((suffix & 0x80) !== 0 && pos === blockLen - 1)
|
|
376
|
+
this.keccak();
|
|
377
|
+
state[blockLen - 1] ^= 0x80;
|
|
378
|
+
this.keccak();
|
|
379
|
+
}
|
|
380
|
+
writeInto(out) {
|
|
381
|
+
aexists(this, false);
|
|
382
|
+
abytes(out);
|
|
383
|
+
this.finish();
|
|
384
|
+
const bufferOut = this.state;
|
|
385
|
+
const { blockLen } = this;
|
|
386
|
+
for (let pos = 0, len = out.length; pos < len;) {
|
|
387
|
+
if (this.posOut >= blockLen)
|
|
388
|
+
this.keccak();
|
|
389
|
+
const take = Math.min(blockLen - this.posOut, len - pos);
|
|
390
|
+
out.set(bufferOut.subarray(this.posOut, this.posOut + take), pos);
|
|
391
|
+
this.posOut += take;
|
|
392
|
+
pos += take;
|
|
393
|
+
}
|
|
394
|
+
return out;
|
|
395
|
+
}
|
|
396
|
+
xofInto(out) {
|
|
397
|
+
// Sha3/Keccak usage with XOF is probably mistake, only SHAKE instances can do XOF
|
|
398
|
+
if (!this.enableXOF)
|
|
399
|
+
throw new Error('XOF is not possible for this instance');
|
|
400
|
+
return this.writeInto(out);
|
|
401
|
+
}
|
|
402
|
+
xof(bytes) {
|
|
403
|
+
anumber(bytes);
|
|
404
|
+
return this.xofInto(new Uint8Array(bytes));
|
|
405
|
+
}
|
|
406
|
+
digestInto(out) {
|
|
407
|
+
aoutput(out, this);
|
|
408
|
+
if (this.finished)
|
|
409
|
+
throw new Error('digest() was already called');
|
|
410
|
+
this.writeInto(out);
|
|
411
|
+
this.destroy();
|
|
412
|
+
return out;
|
|
413
|
+
}
|
|
414
|
+
digest() {
|
|
415
|
+
return this.digestInto(new Uint8Array(this.outputLen));
|
|
416
|
+
}
|
|
417
|
+
destroy() {
|
|
418
|
+
this.destroyed = true;
|
|
419
|
+
clean(this.state);
|
|
420
|
+
}
|
|
421
|
+
_cloneInto(to) {
|
|
422
|
+
const { blockLen, suffix, outputLen, rounds, enableXOF } = this;
|
|
423
|
+
to ||= new Keccak(blockLen, suffix, outputLen, enableXOF, rounds);
|
|
424
|
+
to.state32.set(this.state32);
|
|
425
|
+
to.pos = this.pos;
|
|
426
|
+
to.posOut = this.posOut;
|
|
427
|
+
to.finished = this.finished;
|
|
428
|
+
to.rounds = rounds;
|
|
429
|
+
// Suffix can change in cSHAKE
|
|
430
|
+
to.suffix = suffix;
|
|
431
|
+
to.outputLen = outputLen;
|
|
432
|
+
to.enableXOF = enableXOF;
|
|
433
|
+
to.destroyed = this.destroyed;
|
|
434
|
+
return to;
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
const genShake = (suffix, blockLen, outputLen, info = {}) => createHasher((opts = {}) => new Keccak(blockLen, suffix, opts.dkLen === undefined ? outputLen : opts.dkLen, true), info);
|
|
438
|
+
/** SHAKE128 XOF with 128-bit security. */
|
|
439
|
+
const shake128 =
|
|
440
|
+
/* @__PURE__ */
|
|
441
|
+
genShake(0x1f, 168, 16, /* @__PURE__ */ oidNist(0x0b));
|
|
442
|
+
/** SHAKE256 XOF with 256-bit security. */
|
|
443
|
+
const shake256 =
|
|
444
|
+
/* @__PURE__ */
|
|
445
|
+
genShake(0x1f, 136, 32, /* @__PURE__ */ oidNist(0x0c));
|
|
446
|
+
|
|
70
447
|
/**
|
|
71
448
|
* FIPS 202 SHAKE functions using @noble/hashes
|
|
72
449
|
* Provides streaming XOF (extendable output function) interface
|
|
@@ -87,7 +464,7 @@ class KeccakState {
|
|
|
87
464
|
// SHAKE-128 functions
|
|
88
465
|
|
|
89
466
|
function shake128Init(state) {
|
|
90
|
-
state.hasher =
|
|
467
|
+
state.hasher = shake128.create({});
|
|
91
468
|
state.finalized = false;
|
|
92
469
|
}
|
|
93
470
|
|
|
@@ -109,7 +486,7 @@ function shake128SqueezeBlocks(out, outputOffset, nBlocks, state) {
|
|
|
109
486
|
// SHAKE-256 functions
|
|
110
487
|
|
|
111
488
|
function shake256Init(state) {
|
|
112
|
-
state.hasher =
|
|
489
|
+
state.hasher = shake256.create({});
|
|
113
490
|
state.finalized = false;
|
|
114
491
|
}
|
|
115
492
|
|
|
@@ -176,7 +553,7 @@ function cAddQ(a) {
|
|
|
176
553
|
|
|
177
554
|
function ntt(a) {
|
|
178
555
|
let k = 0;
|
|
179
|
-
let j
|
|
556
|
+
let j;
|
|
180
557
|
|
|
181
558
|
for (let len = 128; len > 0; len >>= 1) {
|
|
182
559
|
for (let start = 0; start < N; start = j + len) {
|
|
@@ -192,7 +569,7 @@ function ntt(a) {
|
|
|
192
569
|
|
|
193
570
|
function invNTTToMont(a) {
|
|
194
571
|
const f = 41978n; // mont^2/256
|
|
195
|
-
let j
|
|
572
|
+
let j;
|
|
196
573
|
let k = 256;
|
|
197
574
|
|
|
198
575
|
for (let len = 1; len < N; len <<= 1) {
|
|
@@ -1109,13 +1486,6 @@ function isZero(buffer) {
|
|
|
1109
1486
|
return acc === 0;
|
|
1110
1487
|
}
|
|
1111
1488
|
|
|
1112
|
-
/**
|
|
1113
|
-
* Default signing context ("ZOND" in ASCII).
|
|
1114
|
-
* Used for domain separation per FIPS 204.
|
|
1115
|
-
* @constant {Uint8Array}
|
|
1116
|
-
*/
|
|
1117
|
-
const DEFAULT_CTX = new Uint8Array([0x5a, 0x4f, 0x4e, 0x44]); // "ZOND"
|
|
1118
|
-
|
|
1119
1489
|
/**
|
|
1120
1490
|
* Convert hex string to Uint8Array with strict validation.
|
|
1121
1491
|
*
|
|
@@ -1148,7 +1518,7 @@ function hexToBytes(hex) {
|
|
|
1148
1518
|
if (!/^[0-9a-fA-F]*$/.test(clean)) {
|
|
1149
1519
|
throw new Error('hex string contains non-hex characters');
|
|
1150
1520
|
}
|
|
1151
|
-
return
|
|
1521
|
+
return hexToBytes$1(clean);
|
|
1152
1522
|
}
|
|
1153
1523
|
|
|
1154
1524
|
function messageToBytes(message) {
|
|
@@ -1189,9 +1559,9 @@ function cryptoSignKeypair(passedSeed, pk, sk) {
|
|
|
1189
1559
|
}
|
|
1190
1560
|
} catch (e) {
|
|
1191
1561
|
if (e instanceof TypeError) {
|
|
1192
|
-
throw new Error(`pk/sk cannot be null
|
|
1562
|
+
throw new Error(`pk/sk cannot be null`, { cause: e });
|
|
1193
1563
|
} else {
|
|
1194
|
-
throw new Error(`${e.message}
|
|
1564
|
+
throw new Error(`${e.message}`, { cause: e });
|
|
1195
1565
|
}
|
|
1196
1566
|
}
|
|
1197
1567
|
|
|
@@ -1213,7 +1583,7 @@ function cryptoSignKeypair(passedSeed, pk, sk) {
|
|
|
1213
1583
|
|
|
1214
1584
|
const outputLength = 2 * SeedBytes + CRHBytes;
|
|
1215
1585
|
const domainSep = new Uint8Array([K, L]);
|
|
1216
|
-
const seedBuf =
|
|
1586
|
+
const seedBuf = shake256.create({}).update(seed).update(domainSep).xof(outputLength);
|
|
1217
1587
|
const rho = seedBuf.slice(0, SeedBytes);
|
|
1218
1588
|
const rhoPrime = seedBuf.slice(SeedBytes, SeedBytes + CRHBytes);
|
|
1219
1589
|
const key = seedBuf.slice(SeedBytes + CRHBytes);
|
|
@@ -1244,7 +1614,7 @@ function cryptoSignKeypair(passedSeed, pk, sk) {
|
|
|
1244
1614
|
packPk(pk, rho, t1);
|
|
1245
1615
|
|
|
1246
1616
|
// Compute tr = SHAKE256(pk) (64 bytes) and write secret key
|
|
1247
|
-
const tr =
|
|
1617
|
+
const tr = shake256.create({}).update(pk).xof(TRBytes);
|
|
1248
1618
|
packSk(sk, rho, tr, key, t0, s1, s2);
|
|
1249
1619
|
|
|
1250
1620
|
return seed;
|
|
@@ -1270,21 +1640,22 @@ function cryptoSignKeypair(passedSeed, pk, sk) {
|
|
|
1270
1640
|
* @param {Uint8Array} sk - Secret key (must be CryptoSecretKeyBytes = 4896 bytes)
|
|
1271
1641
|
* @param {boolean} randomizedSigning - If true, use random nonce for hedged signing.
|
|
1272
1642
|
* If false, use deterministic nonce derived from message and key.
|
|
1273
|
-
* @param {Uint8Array}
|
|
1274
|
-
* Defaults to "ZOND" for QRL compatibility.
|
|
1643
|
+
* @param {Uint8Array} ctx - Context string for domain separation (max 255 bytes).
|
|
1275
1644
|
* @returns {number} 0 on success
|
|
1276
|
-
* @throws {Error} If sk is wrong size or context exceeds 255 bytes
|
|
1645
|
+
* @throws {Error} If ctx is missing, sk is wrong size, or context exceeds 255 bytes
|
|
1277
1646
|
*
|
|
1278
1647
|
* @example
|
|
1279
1648
|
* const sig = new Uint8Array(CryptoBytes);
|
|
1280
|
-
*
|
|
1281
|
-
*
|
|
1282
|
-
* cryptoSignSignature(sig, message, sk, false, new Uint8Array([0x01, 0x02]));
|
|
1649
|
+
* const ctx = new Uint8Array([0x01, 0x02]);
|
|
1650
|
+
* cryptoSignSignature(sig, message, sk, false, ctx);
|
|
1283
1651
|
*/
|
|
1284
|
-
function cryptoSignSignature(sig, m, sk, randomizedSigning, ctx
|
|
1652
|
+
function cryptoSignSignature(sig, m, sk, randomizedSigning, ctx) {
|
|
1285
1653
|
if (!sig || sig.length < CryptoBytes) {
|
|
1286
1654
|
throw new Error(`sig must be at least ${CryptoBytes} bytes`);
|
|
1287
1655
|
}
|
|
1656
|
+
if (!(ctx instanceof Uint8Array)) {
|
|
1657
|
+
throw new TypeError('ctx is required and must be a Uint8Array');
|
|
1658
|
+
}
|
|
1288
1659
|
if (ctx.length > 255) throw new Error(`invalid context length: ${ctx.length} (max 255)`);
|
|
1289
1660
|
if (sk.length !== CryptoSecretKeyBytes) {
|
|
1290
1661
|
throw new Error(`invalid sk length ${sk.length} | Expected length ${CryptoSecretKeyBytes}`);
|
|
@@ -1320,11 +1691,11 @@ function cryptoSignSignature(sig, m, sk, randomizedSigning, ctx = DEFAULT_CTX) {
|
|
|
1320
1691
|
const mBytes = messageToBytes(m);
|
|
1321
1692
|
|
|
1322
1693
|
// mu = SHAKE256(tr || pre || m)
|
|
1323
|
-
const mu =
|
|
1694
|
+
const mu = shake256.create({}).update(tr).update(pre).update(mBytes).xof(CRHBytes);
|
|
1324
1695
|
|
|
1325
1696
|
// rhoPrime = SHAKE256(key || rnd || mu)
|
|
1326
1697
|
const rnd = randomizedSigning ? randomBytes(RNDBytes) : new Uint8Array(RNDBytes);
|
|
1327
|
-
rhoPrime =
|
|
1698
|
+
rhoPrime = shake256.create({}).update(key).update(rnd).update(mu).xof(CRHBytes);
|
|
1328
1699
|
|
|
1329
1700
|
polyVecMatrixExpand(mat, rho);
|
|
1330
1701
|
polyVecLNTT(s1);
|
|
@@ -1346,7 +1717,7 @@ function cryptoSignSignature(sig, m, sk, randomizedSigning, ctx = DEFAULT_CTX) {
|
|
|
1346
1717
|
polyVecKPackW1(sig, w1);
|
|
1347
1718
|
|
|
1348
1719
|
// ctilde = SHAKE256(mu || w1_packed) (64 bytes)
|
|
1349
|
-
const ctilde =
|
|
1720
|
+
const ctilde = shake256
|
|
1350
1721
|
.create({})
|
|
1351
1722
|
.update(mu)
|
|
1352
1723
|
.update(sig.slice(0, K * PolyW1PackedBytes))
|
|
@@ -1411,16 +1782,18 @@ function cryptoSignSignature(sig, m, sk, randomizedSigning, ctx = DEFAULT_CTX) {
|
|
|
1411
1782
|
* @param {string|Uint8Array} msg - Message to sign (hex string, optional 0x prefix, or Uint8Array)
|
|
1412
1783
|
* @param {Uint8Array} sk - Secret key (must be CryptoSecretKeyBytes = 4896 bytes)
|
|
1413
1784
|
* @param {boolean} randomizedSigning - If true, use random nonce; if false, deterministic
|
|
1414
|
-
* @param {Uint8Array}
|
|
1415
|
-
* Defaults to "ZOND" for QRL compatibility.
|
|
1785
|
+
* @param {Uint8Array} ctx - Context string for domain separation (max 255 bytes).
|
|
1416
1786
|
* @returns {Uint8Array} Signed message (CryptoBytes + msg.length bytes)
|
|
1417
1787
|
* @throws {Error} If signing fails
|
|
1418
1788
|
*
|
|
1419
1789
|
* @example
|
|
1420
|
-
* const signedMsg = cryptoSign(message, sk, false);
|
|
1790
|
+
* const signedMsg = cryptoSign(message, sk, false, ctx);
|
|
1421
1791
|
* // signedMsg contains: signature (4627 bytes) || message
|
|
1422
1792
|
*/
|
|
1423
|
-
function cryptoSign(msg, sk, randomizedSigning, ctx
|
|
1793
|
+
function cryptoSign(msg, sk, randomizedSigning, ctx) {
|
|
1794
|
+
if (!(ctx instanceof Uint8Array)) {
|
|
1795
|
+
throw new TypeError('ctx is required and must be a Uint8Array');
|
|
1796
|
+
}
|
|
1424
1797
|
const msgBytes = messageToBytes(msg);
|
|
1425
1798
|
|
|
1426
1799
|
const sm = new Uint8Array(CryptoBytes + msgBytes.length);
|
|
@@ -1447,17 +1820,19 @@ function cryptoSign(msg, sk, randomizedSigning, ctx = DEFAULT_CTX) {
|
|
|
1447
1820
|
* @param {Uint8Array} sig - Signature to verify (must be CryptoBytes = 4627 bytes)
|
|
1448
1821
|
* @param {string|Uint8Array} m - Message that was signed (hex string, optional 0x prefix, or Uint8Array)
|
|
1449
1822
|
* @param {Uint8Array} pk - Public key (must be CryptoPublicKeyBytes = 2592 bytes)
|
|
1450
|
-
* @param {Uint8Array}
|
|
1451
|
-
* Defaults to "ZOND" for QRL compatibility.
|
|
1823
|
+
* @param {Uint8Array} ctx - Context string used during signing (max 255 bytes).
|
|
1452
1824
|
* @returns {boolean} true if signature is valid, false otherwise
|
|
1453
1825
|
*
|
|
1454
1826
|
* @example
|
|
1455
|
-
* const isValid = cryptoSignVerify(signature, message, pk);
|
|
1827
|
+
* const isValid = cryptoSignVerify(signature, message, pk, ctx);
|
|
1456
1828
|
* if (!isValid) {
|
|
1457
1829
|
* throw new Error('Invalid signature');
|
|
1458
1830
|
* }
|
|
1459
1831
|
*/
|
|
1460
|
-
function cryptoSignVerify(sig, m, pk, ctx
|
|
1832
|
+
function cryptoSignVerify(sig, m, pk, ctx) {
|
|
1833
|
+
if (!(ctx instanceof Uint8Array)) {
|
|
1834
|
+
throw new TypeError('ctx is required and must be a Uint8Array');
|
|
1835
|
+
}
|
|
1461
1836
|
if (ctx.length > 255) return false;
|
|
1462
1837
|
let i;
|
|
1463
1838
|
const buf = new Uint8Array(K * PolyW1PackedBytes);
|
|
@@ -1488,7 +1863,7 @@ function cryptoSignVerify(sig, m, pk, ctx = DEFAULT_CTX) {
|
|
|
1488
1863
|
}
|
|
1489
1864
|
|
|
1490
1865
|
/* Compute mu = SHAKE256(tr || pre || m) with tr = SHAKE256(pk) */
|
|
1491
|
-
const tr =
|
|
1866
|
+
const tr = shake256.create({}).update(pk).xof(TRBytes);
|
|
1492
1867
|
|
|
1493
1868
|
const pre = new Uint8Array(2 + ctx.length);
|
|
1494
1869
|
pre[0] = 0;
|
|
@@ -1501,7 +1876,7 @@ function cryptoSignVerify(sig, m, pk, ctx = DEFAULT_CTX) {
|
|
|
1501
1876
|
} catch {
|
|
1502
1877
|
return false;
|
|
1503
1878
|
}
|
|
1504
|
-
const muFull =
|
|
1879
|
+
const muFull = shake256.create({}).update(tr).update(pre).update(mBytes).xof(CRHBytes);
|
|
1505
1880
|
mu.set(muFull);
|
|
1506
1881
|
|
|
1507
1882
|
/* Matrix-vector multiplication; compute Az - c2^dt1 */
|
|
@@ -1526,7 +1901,7 @@ function cryptoSignVerify(sig, m, pk, ctx = DEFAULT_CTX) {
|
|
|
1526
1901
|
polyVecKPackW1(buf, w1);
|
|
1527
1902
|
|
|
1528
1903
|
/* Call random oracle and verify challenge */
|
|
1529
|
-
const c2Hash =
|
|
1904
|
+
const c2Hash = shake256.create({}).update(mu).update(buf).xof(CTILDEBytes);
|
|
1530
1905
|
c2.set(c2Hash);
|
|
1531
1906
|
|
|
1532
1907
|
// Constant-time comparison to prevent timing attacks
|
|
@@ -1545,17 +1920,19 @@ function cryptoSignVerify(sig, m, pk, ctx = DEFAULT_CTX) {
|
|
|
1545
1920
|
*
|
|
1546
1921
|
* @param {Uint8Array} sm - Signed message (signature || message)
|
|
1547
1922
|
* @param {Uint8Array} pk - Public key (must be CryptoPublicKeyBytes = 2592 bytes)
|
|
1548
|
-
* @param {Uint8Array}
|
|
1549
|
-
* Defaults to "ZOND" for QRL compatibility.
|
|
1923
|
+
* @param {Uint8Array} ctx - Context string used during signing (max 255 bytes).
|
|
1550
1924
|
* @returns {Uint8Array|undefined} The original message if valid, undefined if verification fails
|
|
1551
1925
|
*
|
|
1552
1926
|
* @example
|
|
1553
|
-
* const message = cryptoSignOpen(signedMsg, pk);
|
|
1927
|
+
* const message = cryptoSignOpen(signedMsg, pk, ctx);
|
|
1554
1928
|
* if (message === undefined) {
|
|
1555
1929
|
* throw new Error('Invalid signature');
|
|
1556
1930
|
* }
|
|
1557
1931
|
*/
|
|
1558
|
-
function cryptoSignOpen(sm, pk, ctx
|
|
1932
|
+
function cryptoSignOpen(sm, pk, ctx) {
|
|
1933
|
+
if (!(ctx instanceof Uint8Array)) {
|
|
1934
|
+
throw new TypeError('ctx is required and must be a Uint8Array');
|
|
1935
|
+
}
|
|
1559
1936
|
if (sm.length < CryptoBytes) {
|
|
1560
1937
|
return undefined;
|
|
1561
1938
|
}
|
package/dist/mjs/mldsa87.js
CHANGED
|
@@ -174,7 +174,7 @@ function cAddQ(a) {
|
|
|
174
174
|
|
|
175
175
|
function ntt(a) {
|
|
176
176
|
let k = 0;
|
|
177
|
-
let j
|
|
177
|
+
let j;
|
|
178
178
|
|
|
179
179
|
for (let len = 128; len > 0; len >>= 1) {
|
|
180
180
|
for (let start = 0; start < N; start = j + len) {
|
|
@@ -190,7 +190,7 @@ function ntt(a) {
|
|
|
190
190
|
|
|
191
191
|
function invNTTToMont(a) {
|
|
192
192
|
const f = 41978n; // mont^2/256
|
|
193
|
-
let j
|
|
193
|
+
let j;
|
|
194
194
|
let k = 256;
|
|
195
195
|
|
|
196
196
|
for (let len = 1; len < N; len <<= 1) {
|
|
@@ -1107,13 +1107,6 @@ function isZero(buffer) {
|
|
|
1107
1107
|
return acc === 0;
|
|
1108
1108
|
}
|
|
1109
1109
|
|
|
1110
|
-
/**
|
|
1111
|
-
* Default signing context ("ZOND" in ASCII).
|
|
1112
|
-
* Used for domain separation per FIPS 204.
|
|
1113
|
-
* @constant {Uint8Array}
|
|
1114
|
-
*/
|
|
1115
|
-
const DEFAULT_CTX = new Uint8Array([0x5a, 0x4f, 0x4e, 0x44]); // "ZOND"
|
|
1116
|
-
|
|
1117
1110
|
/**
|
|
1118
1111
|
* Convert hex string to Uint8Array with strict validation.
|
|
1119
1112
|
*
|
|
@@ -1187,9 +1180,9 @@ function cryptoSignKeypair(passedSeed, pk, sk) {
|
|
|
1187
1180
|
}
|
|
1188
1181
|
} catch (e) {
|
|
1189
1182
|
if (e instanceof TypeError) {
|
|
1190
|
-
throw new Error(`pk/sk cannot be null
|
|
1183
|
+
throw new Error(`pk/sk cannot be null`, { cause: e });
|
|
1191
1184
|
} else {
|
|
1192
|
-
throw new Error(`${e.message}
|
|
1185
|
+
throw new Error(`${e.message}`, { cause: e });
|
|
1193
1186
|
}
|
|
1194
1187
|
}
|
|
1195
1188
|
|
|
@@ -1268,21 +1261,22 @@ function cryptoSignKeypair(passedSeed, pk, sk) {
|
|
|
1268
1261
|
* @param {Uint8Array} sk - Secret key (must be CryptoSecretKeyBytes = 4896 bytes)
|
|
1269
1262
|
* @param {boolean} randomizedSigning - If true, use random nonce for hedged signing.
|
|
1270
1263
|
* If false, use deterministic nonce derived from message and key.
|
|
1271
|
-
* @param {Uint8Array}
|
|
1272
|
-
* Defaults to "ZOND" for QRL compatibility.
|
|
1264
|
+
* @param {Uint8Array} ctx - Context string for domain separation (max 255 bytes).
|
|
1273
1265
|
* @returns {number} 0 on success
|
|
1274
|
-
* @throws {Error} If sk is wrong size or context exceeds 255 bytes
|
|
1266
|
+
* @throws {Error} If ctx is missing, sk is wrong size, or context exceeds 255 bytes
|
|
1275
1267
|
*
|
|
1276
1268
|
* @example
|
|
1277
1269
|
* const sig = new Uint8Array(CryptoBytes);
|
|
1278
|
-
*
|
|
1279
|
-
*
|
|
1280
|
-
* cryptoSignSignature(sig, message, sk, false, new Uint8Array([0x01, 0x02]));
|
|
1270
|
+
* const ctx = new Uint8Array([0x01, 0x02]);
|
|
1271
|
+
* cryptoSignSignature(sig, message, sk, false, ctx);
|
|
1281
1272
|
*/
|
|
1282
|
-
function cryptoSignSignature(sig, m, sk, randomizedSigning, ctx
|
|
1273
|
+
function cryptoSignSignature(sig, m, sk, randomizedSigning, ctx) {
|
|
1283
1274
|
if (!sig || sig.length < CryptoBytes) {
|
|
1284
1275
|
throw new Error(`sig must be at least ${CryptoBytes} bytes`);
|
|
1285
1276
|
}
|
|
1277
|
+
if (!(ctx instanceof Uint8Array)) {
|
|
1278
|
+
throw new TypeError('ctx is required and must be a Uint8Array');
|
|
1279
|
+
}
|
|
1286
1280
|
if (ctx.length > 255) throw new Error(`invalid context length: ${ctx.length} (max 255)`);
|
|
1287
1281
|
if (sk.length !== CryptoSecretKeyBytes) {
|
|
1288
1282
|
throw new Error(`invalid sk length ${sk.length} | Expected length ${CryptoSecretKeyBytes}`);
|
|
@@ -1409,16 +1403,18 @@ function cryptoSignSignature(sig, m, sk, randomizedSigning, ctx = DEFAULT_CTX) {
|
|
|
1409
1403
|
* @param {string|Uint8Array} msg - Message to sign (hex string, optional 0x prefix, or Uint8Array)
|
|
1410
1404
|
* @param {Uint8Array} sk - Secret key (must be CryptoSecretKeyBytes = 4896 bytes)
|
|
1411
1405
|
* @param {boolean} randomizedSigning - If true, use random nonce; if false, deterministic
|
|
1412
|
-
* @param {Uint8Array}
|
|
1413
|
-
* Defaults to "ZOND" for QRL compatibility.
|
|
1406
|
+
* @param {Uint8Array} ctx - Context string for domain separation (max 255 bytes).
|
|
1414
1407
|
* @returns {Uint8Array} Signed message (CryptoBytes + msg.length bytes)
|
|
1415
1408
|
* @throws {Error} If signing fails
|
|
1416
1409
|
*
|
|
1417
1410
|
* @example
|
|
1418
|
-
* const signedMsg = cryptoSign(message, sk, false);
|
|
1411
|
+
* const signedMsg = cryptoSign(message, sk, false, ctx);
|
|
1419
1412
|
* // signedMsg contains: signature (4627 bytes) || message
|
|
1420
1413
|
*/
|
|
1421
|
-
function cryptoSign(msg, sk, randomizedSigning, ctx
|
|
1414
|
+
function cryptoSign(msg, sk, randomizedSigning, ctx) {
|
|
1415
|
+
if (!(ctx instanceof Uint8Array)) {
|
|
1416
|
+
throw new TypeError('ctx is required and must be a Uint8Array');
|
|
1417
|
+
}
|
|
1422
1418
|
const msgBytes = messageToBytes(msg);
|
|
1423
1419
|
|
|
1424
1420
|
const sm = new Uint8Array(CryptoBytes + msgBytes.length);
|
|
@@ -1445,17 +1441,19 @@ function cryptoSign(msg, sk, randomizedSigning, ctx = DEFAULT_CTX) {
|
|
|
1445
1441
|
* @param {Uint8Array} sig - Signature to verify (must be CryptoBytes = 4627 bytes)
|
|
1446
1442
|
* @param {string|Uint8Array} m - Message that was signed (hex string, optional 0x prefix, or Uint8Array)
|
|
1447
1443
|
* @param {Uint8Array} pk - Public key (must be CryptoPublicKeyBytes = 2592 bytes)
|
|
1448
|
-
* @param {Uint8Array}
|
|
1449
|
-
* Defaults to "ZOND" for QRL compatibility.
|
|
1444
|
+
* @param {Uint8Array} ctx - Context string used during signing (max 255 bytes).
|
|
1450
1445
|
* @returns {boolean} true if signature is valid, false otherwise
|
|
1451
1446
|
*
|
|
1452
1447
|
* @example
|
|
1453
|
-
* const isValid = cryptoSignVerify(signature, message, pk);
|
|
1448
|
+
* const isValid = cryptoSignVerify(signature, message, pk, ctx);
|
|
1454
1449
|
* if (!isValid) {
|
|
1455
1450
|
* throw new Error('Invalid signature');
|
|
1456
1451
|
* }
|
|
1457
1452
|
*/
|
|
1458
|
-
function cryptoSignVerify(sig, m, pk, ctx
|
|
1453
|
+
function cryptoSignVerify(sig, m, pk, ctx) {
|
|
1454
|
+
if (!(ctx instanceof Uint8Array)) {
|
|
1455
|
+
throw new TypeError('ctx is required and must be a Uint8Array');
|
|
1456
|
+
}
|
|
1459
1457
|
if (ctx.length > 255) return false;
|
|
1460
1458
|
let i;
|
|
1461
1459
|
const buf = new Uint8Array(K * PolyW1PackedBytes);
|
|
@@ -1543,17 +1541,19 @@ function cryptoSignVerify(sig, m, pk, ctx = DEFAULT_CTX) {
|
|
|
1543
1541
|
*
|
|
1544
1542
|
* @param {Uint8Array} sm - Signed message (signature || message)
|
|
1545
1543
|
* @param {Uint8Array} pk - Public key (must be CryptoPublicKeyBytes = 2592 bytes)
|
|
1546
|
-
* @param {Uint8Array}
|
|
1547
|
-
* Defaults to "ZOND" for QRL compatibility.
|
|
1544
|
+
* @param {Uint8Array} ctx - Context string used during signing (max 255 bytes).
|
|
1548
1545
|
* @returns {Uint8Array|undefined} The original message if valid, undefined if verification fails
|
|
1549
1546
|
*
|
|
1550
1547
|
* @example
|
|
1551
|
-
* const message = cryptoSignOpen(signedMsg, pk);
|
|
1548
|
+
* const message = cryptoSignOpen(signedMsg, pk, ctx);
|
|
1552
1549
|
* if (message === undefined) {
|
|
1553
1550
|
* throw new Error('Invalid signature');
|
|
1554
1551
|
* }
|
|
1555
1552
|
*/
|
|
1556
|
-
function cryptoSignOpen(sm, pk, ctx
|
|
1553
|
+
function cryptoSignOpen(sm, pk, ctx) {
|
|
1554
|
+
if (!(ctx instanceof Uint8Array)) {
|
|
1555
|
+
throw new TypeError('ctx is required and must be a Uint8Array');
|
|
1556
|
+
}
|
|
1557
1557
|
if (sm.length < CryptoBytes) {
|
|
1558
1558
|
return undefined;
|
|
1559
1559
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@theqrl/mldsa87",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.0",
|
|
4
4
|
"description": "ML-DSA-87 cryptography",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ml-dsa",
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
"scripts": {
|
|
34
34
|
"test": "../../node_modules/mocha/bin/mocha.js --require ../../scripts/node-test-setup.cjs --timeout 10000",
|
|
35
35
|
"test:browser": "playwright test",
|
|
36
|
-
"build": "rollup
|
|
36
|
+
"build": "rollup -c && ./fixup",
|
|
37
37
|
"lint-check": "eslint 'src/**/*.js' 'test/**/*.js'",
|
|
38
38
|
"lint": "eslint --fix 'src/**/*.js' 'test/**/*.js'",
|
|
39
39
|
"coverage": "c8 npm run test",
|
|
@@ -53,19 +53,39 @@
|
|
|
53
53
|
"node": ">=20.19.0"
|
|
54
54
|
},
|
|
55
55
|
"devDependencies": {
|
|
56
|
-
"@eslint/js": "
|
|
57
|
-
"
|
|
58
|
-
"
|
|
59
|
-
"
|
|
60
|
-
"eslint
|
|
61
|
-
"eslint-
|
|
62
|
-
"eslint-plugin-
|
|
63
|
-
"
|
|
64
|
-
"
|
|
65
|
-
"
|
|
66
|
-
"
|
|
56
|
+
"@eslint/js": "10.0.1",
|
|
57
|
+
"@rollup/plugin-node-resolve": "16.0.3",
|
|
58
|
+
"c8": "11.0.0",
|
|
59
|
+
"chai": "6.2.2",
|
|
60
|
+
"eslint": "10.0.3",
|
|
61
|
+
"eslint-config-prettier": "10.1.8",
|
|
62
|
+
"eslint-plugin-import-x": "4.16.2",
|
|
63
|
+
"eslint-plugin-prettier": "5.5.5",
|
|
64
|
+
"globals": "17.4.0",
|
|
65
|
+
"minimatch": "10.2.4",
|
|
66
|
+
"mocha": "11.7.5",
|
|
67
|
+
"prettier": "3.8.1",
|
|
68
|
+
"rollup": "4.59.0",
|
|
69
|
+
"serialize-javascript": "7.0.4",
|
|
70
|
+
"tar": "7.5.11"
|
|
67
71
|
},
|
|
68
72
|
"dependencies": {
|
|
69
|
-
"@noble/hashes": "
|
|
73
|
+
"@noble/hashes": "2.0.1"
|
|
74
|
+
},
|
|
75
|
+
"overrides": {
|
|
76
|
+
"diff": "8.0.3",
|
|
77
|
+
"minimatch": "10.2.4"
|
|
78
|
+
},
|
|
79
|
+
"c8": {
|
|
80
|
+
"include": [
|
|
81
|
+
"src/**"
|
|
82
|
+
],
|
|
83
|
+
"exclude": [
|
|
84
|
+
"**/dist/**",
|
|
85
|
+
"**/test/**",
|
|
86
|
+
"**/browser-tests/**",
|
|
87
|
+
"**/*.d.ts"
|
|
88
|
+
],
|
|
89
|
+
"all": true
|
|
70
90
|
}
|
|
71
91
|
}
|
package/src/index.d.ts
CHANGED
|
@@ -56,12 +56,12 @@ export function cryptoSignKeypair(
|
|
|
56
56
|
): Uint8Array;
|
|
57
57
|
|
|
58
58
|
/**
|
|
59
|
-
* Create a signature for a message
|
|
59
|
+
* Create a signature for a message
|
|
60
60
|
* @param sig - Output buffer for signature (must be CryptoBytes length minimum)
|
|
61
61
|
* @param m - Message to sign (hex string or Uint8Array; strings are parsed as hex only)
|
|
62
62
|
* @param sk - Secret key
|
|
63
63
|
* @param randomizedSigning - If true, use random nonce; if false, deterministic
|
|
64
|
-
* @param ctx -
|
|
64
|
+
* @param ctx - Context string (max 255 bytes)
|
|
65
65
|
* @returns 0 on success
|
|
66
66
|
* @throws Error if sk is wrong size or context too long
|
|
67
67
|
*/
|
|
@@ -70,7 +70,7 @@ export function cryptoSignSignature(
|
|
|
70
70
|
m: Uint8Array | string,
|
|
71
71
|
sk: Uint8Array,
|
|
72
72
|
randomizedSigning: boolean,
|
|
73
|
-
ctx
|
|
73
|
+
ctx: Uint8Array
|
|
74
74
|
): number;
|
|
75
75
|
|
|
76
76
|
/**
|
|
@@ -78,7 +78,7 @@ export function cryptoSignSignature(
|
|
|
78
78
|
* @param msg - Message to sign
|
|
79
79
|
* @param sk - Secret key
|
|
80
80
|
* @param randomizedSigning - If true, use random nonce; if false, deterministic
|
|
81
|
-
* @param ctx -
|
|
81
|
+
* @param ctx - Context string (max 255 bytes)
|
|
82
82
|
* @returns Signed message (signature || message)
|
|
83
83
|
* @throws Error if signing fails
|
|
84
84
|
*/
|
|
@@ -86,35 +86,35 @@ export function cryptoSign(
|
|
|
86
86
|
msg: Uint8Array | string,
|
|
87
87
|
sk: Uint8Array,
|
|
88
88
|
randomizedSigning: boolean,
|
|
89
|
-
ctx
|
|
89
|
+
ctx: Uint8Array
|
|
90
90
|
): Uint8Array;
|
|
91
91
|
|
|
92
92
|
/**
|
|
93
|
-
* Verify a signature
|
|
93
|
+
* Verify a signature
|
|
94
94
|
* @param sig - Signature to verify
|
|
95
95
|
* @param m - Message that was signed (hex string or Uint8Array; strings are parsed as hex only)
|
|
96
96
|
* @param pk - Public key
|
|
97
|
-
* @param ctx -
|
|
97
|
+
* @param ctx - Context string (max 255 bytes)
|
|
98
98
|
* @returns true if signature is valid, false otherwise
|
|
99
99
|
*/
|
|
100
100
|
export function cryptoSignVerify(
|
|
101
101
|
sig: Uint8Array,
|
|
102
102
|
m: Uint8Array | string,
|
|
103
103
|
pk: Uint8Array,
|
|
104
|
-
ctx
|
|
104
|
+
ctx: Uint8Array
|
|
105
105
|
): boolean;
|
|
106
106
|
|
|
107
107
|
/**
|
|
108
108
|
* Open a signed message (verify and extract message)
|
|
109
109
|
* @param sm - Signed message (signature || message)
|
|
110
110
|
* @param pk - Public key
|
|
111
|
-
* @param ctx -
|
|
111
|
+
* @param ctx - Context string (max 255 bytes)
|
|
112
112
|
* @returns Message if valid, undefined if verification fails
|
|
113
113
|
*/
|
|
114
114
|
export function cryptoSignOpen(
|
|
115
115
|
sm: Uint8Array,
|
|
116
116
|
pk: Uint8Array,
|
|
117
|
-
ctx
|
|
117
|
+
ctx: Uint8Array
|
|
118
118
|
): Uint8Array | undefined;
|
|
119
119
|
|
|
120
120
|
// Utility functions
|