@noble/post-quantum 0.6.1 → 0.7.1
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 +191 -104
- package/_crystals.d.ts +8 -3
- package/_crystals.js +38 -10
- package/falcon.d.ts +1 -2
- package/falcon.js +202 -115
- package/hybrid.d.ts +38 -28
- package/hybrid.js +215 -86
- package/index.d.ts +0 -1
- package/index.js +1 -2
- package/ml-dsa.d.ts +31 -5
- package/ml-dsa.js +118 -34
- package/ml-kem.d.ts +45 -4
- package/ml-kem.js +206 -64
- package/package.json +16 -20
- package/slh-dsa.d.ts +23 -3
- package/slh-dsa.js +127 -60
- package/src/_crystals.ts +45 -11
- package/src/falcon.ts +206 -121
- package/src/hybrid.ts +217 -83
- package/src/index.ts +1 -1
- package/src/ml-dsa.ts +140 -43
- package/src/ml-kem.ts +239 -66
- package/src/slh-dsa.ts +149 -69
- package/src/utils.ts +186 -25
- package/src/webcrypto.ts +322 -0
- package/utils.d.ts +54 -4
- package/utils.js +167 -28
- package/webcrypto.d.ts +91 -0
- package/webcrypto.js +213 -0
- package/_crystals.d.ts.map +0 -1
- package/_crystals.js.map +0 -1
- package/falcon.d.ts.map +0 -1
- package/falcon.js.map +0 -1
- package/hybrid.d.ts.map +0 -1
- package/hybrid.js.map +0 -1
- package/index.d.ts.map +0 -1
- package/index.js.map +0 -1
- package/ml-dsa.d.ts.map +0 -1
- package/ml-dsa.js.map +0 -1
- package/ml-kem.d.ts.map +0 -1
- package/ml-kem.js.map +0 -1
- package/slh-dsa.d.ts.map +0 -1
- package/slh-dsa.js.map +0 -1
- package/utils.d.ts.map +0 -1
- package/utils.js.map +0 -1
package/utils.d.ts
CHANGED
|
@@ -84,9 +84,11 @@ export { concatBytesDoc as concatBytes };
|
|
|
84
84
|
* ```
|
|
85
85
|
*/
|
|
86
86
|
export declare const randomBytes: typeof randb;
|
|
87
|
+
export declare function aarray<T>(item: unknown, title: string, inner?: (elm: T, title: string) => void): T[];
|
|
88
|
+
export declare function aobject<T extends object>(value: unknown, title?: string): T;
|
|
87
89
|
/**
|
|
88
90
|
* Compares two byte arrays in a length-constant way for equal lengths.
|
|
89
|
-
*
|
|
91
|
+
* Inputs are validated as byte arrays; unequal lengths return `false` immediately.
|
|
90
92
|
* @param a - First byte array.
|
|
91
93
|
* @param b - Second byte array.
|
|
92
94
|
* @returns Whether both arrays contain the same bytes.
|
|
@@ -184,32 +186,66 @@ export type SigOpts = VerOpts & {
|
|
|
184
186
|
* ```
|
|
185
187
|
*/
|
|
186
188
|
export declare function validateOpts(opts: object): void;
|
|
189
|
+
/** Keys accepted by `verify`. */
|
|
190
|
+
export declare const VER_OPT_KEYS: readonly ['context'];
|
|
191
|
+
/** Keys accepted by `sign`. */
|
|
192
|
+
export declare const SIG_OPT_KEYS: readonly ['context', 'extraEntropy'];
|
|
193
|
+
/**
|
|
194
|
+
* Rejects option keys the caller did not mean to set.
|
|
195
|
+
*
|
|
196
|
+
* Validating the types of known keys while ignoring unknown ones makes a typo
|
|
197
|
+
* indistinguishable from an omission, and for these options an omission is a
|
|
198
|
+
* security downgrade rather than a no-op: `{ ctx }` instead of `{ context }` signs
|
|
199
|
+
* with no domain separation, succeeds, and verifies for anyone who also supplies
|
|
200
|
+
* none. Nothing at any layer reports it. TypeScript catches this through excess
|
|
201
|
+
* property checks, so the exposure is JavaScript callers specifically.
|
|
202
|
+
*
|
|
203
|
+
* @param opts - Options object to check.
|
|
204
|
+
* @param allowed - The keys this call site accepts.
|
|
205
|
+
* Returns a frozen null-prototype snapshot so later reads cannot fall through to a polluted
|
|
206
|
+
* prototype. Like `checkOpts()` in noble-hashes, only enumerable own properties are copied.
|
|
207
|
+
* @throws If any other copied key is present or the bag has a custom prototype. {@link TypeError}
|
|
208
|
+
* @returns Sanitized snapshot of the enumerable own options.
|
|
209
|
+
* @example
|
|
210
|
+
* Accept a known option key. A key the list does not name, such as `ctx`, throws instead.
|
|
211
|
+
* ```ts
|
|
212
|
+
* import { checkOptKeys } from '@noble/post-quantum/utils.js';
|
|
213
|
+
* checkOptKeys({ context: new Uint8Array() }, ['context']);
|
|
214
|
+
* ```
|
|
215
|
+
*/
|
|
216
|
+
export declare function checkOptKeys<T extends object>(opts: T, allowed: readonly string[]): T;
|
|
187
217
|
/**
|
|
188
218
|
* Validates common verification options.
|
|
189
219
|
* `context` itself is validated with `abytes(...)`, and individual algorithms may narrow support
|
|
190
220
|
* further after this shared plain-object gate.
|
|
191
221
|
* @param opts - Verification options. See {@link VerOpts}.
|
|
222
|
+
* @param allowed - Keys this call site accepts. Defaults to {@link VER_OPT_KEYS}; surfaces that
|
|
223
|
+
* take extra keys, or take fewer, pass their own list.
|
|
192
224
|
* @throws On wrong argument types. {@link TypeError}
|
|
225
|
+
* @returns Frozen null-prototype snapshot of the validated options.
|
|
193
226
|
* @example
|
|
194
227
|
* Validate common verification options.
|
|
195
228
|
* ```ts
|
|
196
229
|
* validateVerOpts({ context: new Uint8Array([1]) });
|
|
197
230
|
* ```
|
|
198
231
|
*/
|
|
199
|
-
export declare function validateVerOpts(opts:
|
|
232
|
+
export declare function validateVerOpts<T extends TArg<VerOpts>>(opts: T, allowed?: readonly string[]): T;
|
|
200
233
|
/**
|
|
201
234
|
* Validates common signing options.
|
|
202
235
|
* `extraEntropy` is validated with `abytes(...)`; exact lengths and extra algorithm-specific
|
|
203
236
|
* restrictions are enforced later by callers.
|
|
204
237
|
* @param opts - Signing options. See {@link SigOpts}.
|
|
238
|
+
* @param allowed - Keys this call site accepts. Defaults to {@link SIG_OPT_KEYS}; surfaces that
|
|
239
|
+
* take extra keys, or take fewer, pass their own list.
|
|
205
240
|
* @throws On wrong argument types. {@link TypeError}
|
|
241
|
+
* @returns Frozen null-prototype snapshot of the validated options.
|
|
206
242
|
* @example
|
|
207
243
|
* Validate common signing options.
|
|
208
244
|
* ```ts
|
|
209
245
|
* validateSigOpts({ extraEntropy: new Uint8Array([1]) });
|
|
210
246
|
* ```
|
|
211
247
|
*/
|
|
212
|
-
export declare function validateSigOpts(opts:
|
|
248
|
+
export declare function validateSigOpts<T extends TArg<SigOpts>>(opts: T, allowed?: readonly string[]): T;
|
|
213
249
|
/** Generic signature interface with key generation, signing, and verification. */
|
|
214
250
|
export type Signer = CryptoKeys & {
|
|
215
251
|
/** Public byte lengths for signatures and signing randomness. */
|
|
@@ -356,6 +392,7 @@ export declare function cleanBytes(...list: (TypedArray | TypedArray[])[]): void
|
|
|
356
392
|
* Creates a 32-bit mask with the lowest `bits` bits set.
|
|
357
393
|
* @param bits - Number of low bits to keep.
|
|
358
394
|
* @returns Bit mask with `bits` ones.
|
|
395
|
+
* @throws On wrong argument types. {@link TypeError}
|
|
359
396
|
* @throws On wrong argument ranges or values. {@link RangeError}
|
|
360
397
|
* @example
|
|
361
398
|
* Create a low-bit mask for packed-field operations.
|
|
@@ -417,4 +454,17 @@ export declare function checkHash(hash: CHash, requiredStrength?: number): void;
|
|
|
417
454
|
* ```
|
|
418
455
|
*/
|
|
419
456
|
export declare function getMessagePrehash(hash: CHash, msg: TArg<Uint8Array>, ctx?: TArg<Uint8Array>): TRet<Uint8Array>;
|
|
420
|
-
|
|
457
|
+
/**
|
|
458
|
+
* Asserts something is a string.
|
|
459
|
+
* @param value - Value to validate.
|
|
460
|
+
* @param title - Label included in thrown errors.
|
|
461
|
+
* @returns The validated string.
|
|
462
|
+
* @throws On wrong argument types. {@link TypeError}
|
|
463
|
+
* @example
|
|
464
|
+
* Validate a label string.
|
|
465
|
+
*
|
|
466
|
+
* ```ts
|
|
467
|
+
* astring('example', 'label');
|
|
468
|
+
* ```
|
|
469
|
+
*/
|
|
470
|
+
export declare function astring(value: unknown, title?: string): string;
|
package/utils.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* @module
|
|
4
4
|
*/
|
|
5
5
|
/*! noble-post-quantum - MIT License (c) 2024 Paul Miller (paulmillr.com) */
|
|
6
|
-
import { abytes, abytes as abytes_, concatBytes, isLE, randomBytes as randb, } from '@noble/hashes/utils.js';
|
|
6
|
+
import { abytes, abytes as abytes_, ahash as ahash_, anumber, bytesToHex, concatBytes, isBytes, isLE, randomBytes as randb, } from '@noble/hashes/utils.js';
|
|
7
7
|
/**
|
|
8
8
|
* Asserts that a value is a byte array and optionally checks its length.
|
|
9
9
|
* Returns the original reference unchanged on success, and currently also accepts Node `Buffer`
|
|
@@ -44,9 +44,23 @@ export { concatBytesDoc as concatBytes };
|
|
|
44
44
|
* ```
|
|
45
45
|
*/
|
|
46
46
|
export const randomBytes = randb;
|
|
47
|
+
export function aarray(item, title, inner = () => { }) {
|
|
48
|
+
if (!Array.isArray(item))
|
|
49
|
+
throw new TypeError(`"${title}" expected array, got type=${typeof item}`);
|
|
50
|
+
for (let i = 0; i < item.length; i++)
|
|
51
|
+
inner(item[i], `${title}[${i}]`);
|
|
52
|
+
return item;
|
|
53
|
+
}
|
|
54
|
+
export function aobject(value, title = 'object') {
|
|
55
|
+
if (value === null || typeof value !== 'object' || Array.isArray(value))
|
|
56
|
+
throw new TypeError(title === 'object'
|
|
57
|
+
? 'expected valid options object'
|
|
58
|
+
: `"${title}" expected object, got type=${typeof value}`);
|
|
59
|
+
return value;
|
|
60
|
+
}
|
|
47
61
|
/**
|
|
48
62
|
* Compares two byte arrays in a length-constant way for equal lengths.
|
|
49
|
-
*
|
|
63
|
+
* Inputs are validated as byte arrays; unequal lengths return `false` immediately.
|
|
50
64
|
* @param a - First byte array.
|
|
51
65
|
* @param b - Second byte array.
|
|
52
66
|
* @returns Whether both arrays contain the same bytes.
|
|
@@ -57,6 +71,8 @@ export const randomBytes = randb;
|
|
|
57
71
|
* ```
|
|
58
72
|
*/
|
|
59
73
|
export function equalBytes(a, b) {
|
|
74
|
+
a = abytes(a);
|
|
75
|
+
b = abytes(b);
|
|
60
76
|
if (a.length !== b.length)
|
|
61
77
|
return false;
|
|
62
78
|
let diff = 0;
|
|
@@ -76,9 +92,10 @@ export function equalBytes(a, b) {
|
|
|
76
92
|
* ```
|
|
77
93
|
*/
|
|
78
94
|
export function copyBytes(bytes) {
|
|
79
|
-
//
|
|
80
|
-
//
|
|
81
|
-
|
|
95
|
+
// The typed-array constructor copies a typed-array source through its internal byte storage.
|
|
96
|
+
// Unlike `Uint8Array.from`, it does not invoke a subclass-overridden iterator. Keep the explicit
|
|
97
|
+
// validation because the constructor itself would also accept arrays and other typed arrays.
|
|
98
|
+
return new Uint8Array(abytes(bytes));
|
|
82
99
|
}
|
|
83
100
|
/**
|
|
84
101
|
* Byte-swaps each 64-bit lane in place.
|
|
@@ -137,42 +154,110 @@ export const baswap64If = isLE
|
|
|
137
154
|
*/
|
|
138
155
|
export function validateOpts(opts) {
|
|
139
156
|
// Arrays silently passed here before, but these call sites expect named option-bag fields.
|
|
140
|
-
if (
|
|
141
|
-
throw new TypeError('expected
|
|
157
|
+
if (isBytes(opts))
|
|
158
|
+
throw new TypeError('"opts" expected object, got Uint8Array');
|
|
159
|
+
aobject(opts, 'opts');
|
|
160
|
+
const proto = Object.getPrototypeOf(opts);
|
|
161
|
+
// Options are security parameters, not general class instances. Restricting the bag to own
|
|
162
|
+
// properties prevents values injected through Object.prototype (or a custom shared prototype)
|
|
163
|
+
// from silently changing signing behavior. Null-prototype records remain supported.
|
|
164
|
+
if (proto !== null && proto !== Object.prototype)
|
|
165
|
+
throw new TypeError('"opts" expected a plain object');
|
|
166
|
+
}
|
|
167
|
+
// Frozen because they are exported: an unfrozen array export lets anything in the
|
|
168
|
+
// process push a key onto the accepted set and silently re-open exactly the hole this
|
|
169
|
+
// validation closes.
|
|
170
|
+
/** Keys accepted by `verify`. */
|
|
171
|
+
export const VER_OPT_KEYS = /* @__PURE__ */ Object.freeze([
|
|
172
|
+
'context',
|
|
173
|
+
]);
|
|
174
|
+
/** Keys accepted by `sign`. */
|
|
175
|
+
export const SIG_OPT_KEYS = /* @__PURE__ */ Object.freeze([
|
|
176
|
+
'context',
|
|
177
|
+
'extraEntropy',
|
|
178
|
+
]);
|
|
179
|
+
/**
|
|
180
|
+
* Rejects option keys the caller did not mean to set.
|
|
181
|
+
*
|
|
182
|
+
* Validating the types of known keys while ignoring unknown ones makes a typo
|
|
183
|
+
* indistinguishable from an omission, and for these options an omission is a
|
|
184
|
+
* security downgrade rather than a no-op: `{ ctx }` instead of `{ context }` signs
|
|
185
|
+
* with no domain separation, succeeds, and verifies for anyone who also supplies
|
|
186
|
+
* none. Nothing at any layer reports it. TypeScript catches this through excess
|
|
187
|
+
* property checks, so the exposure is JavaScript callers specifically.
|
|
188
|
+
*
|
|
189
|
+
* @param opts - Options object to check.
|
|
190
|
+
* @param allowed - The keys this call site accepts.
|
|
191
|
+
* Returns a frozen null-prototype snapshot so later reads cannot fall through to a polluted
|
|
192
|
+
* prototype. Like `checkOpts()` in noble-hashes, only enumerable own properties are copied.
|
|
193
|
+
* @throws If any other copied key is present or the bag has a custom prototype. {@link TypeError}
|
|
194
|
+
* @returns Sanitized snapshot of the enumerable own options.
|
|
195
|
+
* @example
|
|
196
|
+
* Accept a known option key. A key the list does not name, such as `ctx`, throws instead.
|
|
197
|
+
* ```ts
|
|
198
|
+
* import { checkOptKeys } from '@noble/post-quantum/utils.js';
|
|
199
|
+
* checkOptKeys({ context: new Uint8Array() }, ['context']);
|
|
200
|
+
* ```
|
|
201
|
+
*/
|
|
202
|
+
export function checkOptKeys(opts, allowed) {
|
|
203
|
+
validateOpts(opts);
|
|
204
|
+
// Snapshot once before validation: Object.assign follows the same own-enumerable option-bag
|
|
205
|
+
// semantics as noble-hashes, while the null prototype keeps omitted fields immune to pollution.
|
|
206
|
+
const normalized = Object.assign(Object.create(null), opts);
|
|
207
|
+
for (const [k, v] of Object.entries(normalized)) {
|
|
208
|
+
// `undefined` means unset everywhere else in these validators, and building an options bag by
|
|
209
|
+
// spread is a normal way to reach these calls, so present-but-undefined stays equivalent to
|
|
210
|
+
// omission.
|
|
211
|
+
if (v === undefined)
|
|
212
|
+
continue;
|
|
213
|
+
if (!allowed.includes(k))
|
|
214
|
+
throw new TypeError('unexpected option "' + String(k) + '"; expected one of: ' + allowed.join(', '));
|
|
215
|
+
}
|
|
216
|
+
return Object.freeze(normalized);
|
|
142
217
|
}
|
|
143
218
|
/**
|
|
144
219
|
* Validates common verification options.
|
|
145
220
|
* `context` itself is validated with `abytes(...)`, and individual algorithms may narrow support
|
|
146
221
|
* further after this shared plain-object gate.
|
|
147
222
|
* @param opts - Verification options. See {@link VerOpts}.
|
|
223
|
+
* @param allowed - Keys this call site accepts. Defaults to {@link VER_OPT_KEYS}; surfaces that
|
|
224
|
+
* take extra keys, or take fewer, pass their own list.
|
|
148
225
|
* @throws On wrong argument types. {@link TypeError}
|
|
226
|
+
* @returns Frozen null-prototype snapshot of the validated options.
|
|
149
227
|
* @example
|
|
150
228
|
* Validate common verification options.
|
|
151
229
|
* ```ts
|
|
152
230
|
* validateVerOpts({ context: new Uint8Array([1]) });
|
|
153
231
|
* ```
|
|
154
232
|
*/
|
|
155
|
-
export function validateVerOpts(opts) {
|
|
156
|
-
|
|
157
|
-
if (
|
|
158
|
-
abytes(
|
|
233
|
+
export function validateVerOpts(opts, allowed = VER_OPT_KEYS) {
|
|
234
|
+
const normalized = checkOptKeys(opts, allowed);
|
|
235
|
+
if (normalized.context !== undefined)
|
|
236
|
+
abytes(normalized.context, undefined, 'opts.context');
|
|
237
|
+
return normalized;
|
|
159
238
|
}
|
|
160
239
|
/**
|
|
161
240
|
* Validates common signing options.
|
|
162
241
|
* `extraEntropy` is validated with `abytes(...)`; exact lengths and extra algorithm-specific
|
|
163
242
|
* restrictions are enforced later by callers.
|
|
164
243
|
* @param opts - Signing options. See {@link SigOpts}.
|
|
244
|
+
* @param allowed - Keys this call site accepts. Defaults to {@link SIG_OPT_KEYS}; surfaces that
|
|
245
|
+
* take extra keys, or take fewer, pass their own list.
|
|
165
246
|
* @throws On wrong argument types. {@link TypeError}
|
|
247
|
+
* @returns Frozen null-prototype snapshot of the validated options.
|
|
166
248
|
* @example
|
|
167
249
|
* Validate common signing options.
|
|
168
250
|
* ```ts
|
|
169
251
|
* validateSigOpts({ extraEntropy: new Uint8Array([1]) });
|
|
170
252
|
* ```
|
|
171
253
|
*/
|
|
172
|
-
export function validateSigOpts(opts) {
|
|
173
|
-
|
|
174
|
-
if (
|
|
175
|
-
abytes(
|
|
254
|
+
export function validateSigOpts(opts, allowed = SIG_OPT_KEYS) {
|
|
255
|
+
const normalized = checkOptKeys(opts, allowed);
|
|
256
|
+
if (normalized.context !== undefined)
|
|
257
|
+
abytes(normalized.context, undefined, 'opts.context');
|
|
258
|
+
if (normalized.extraEntropy !== false && normalized.extraEntropy !== undefined)
|
|
259
|
+
abytes(normalized.extraEntropy, undefined, 'opts.extraEntropy');
|
|
260
|
+
return normalized;
|
|
176
261
|
}
|
|
177
262
|
/**
|
|
178
263
|
* Builds a fixed-layout coder from byte lengths and nested coders.
|
|
@@ -247,11 +332,12 @@ export function vecCoder(c, vecLen) {
|
|
|
247
332
|
return {
|
|
248
333
|
bytesLen,
|
|
249
334
|
encode: (u) => {
|
|
250
|
-
|
|
251
|
-
|
|
335
|
+
const uArr = aarray(u, 'u');
|
|
336
|
+
if (uArr.length !== vecLen)
|
|
337
|
+
throw new RangeError(`vecCoder.encode: wrong length=${uArr.length}. Expected: ${vecLen}`);
|
|
252
338
|
const res = new Uint8Array(bytesLen);
|
|
253
|
-
for (let i = 0, pos = 0; i <
|
|
254
|
-
const b = coder.encode(
|
|
339
|
+
for (let i = 0, pos = 0; i < uArr.length; i++) {
|
|
340
|
+
const b = coder.encode(uArr[i]);
|
|
255
341
|
res.set(b, pos);
|
|
256
342
|
b.fill(0); // clean
|
|
257
343
|
pos += b.length;
|
|
@@ -291,6 +377,7 @@ export function cleanBytes(...list) {
|
|
|
291
377
|
* Creates a 32-bit mask with the lowest `bits` bits set.
|
|
292
378
|
* @param bits - Number of low bits to keep.
|
|
293
379
|
* @returns Bit mask with `bits` ones.
|
|
380
|
+
* @throws On wrong argument types. {@link TypeError}
|
|
294
381
|
* @throws On wrong argument ranges or values. {@link RangeError}
|
|
295
382
|
* @example
|
|
296
383
|
* Create a low-bit mask for packed-field operations.
|
|
@@ -299,8 +386,9 @@ export function cleanBytes(...list) {
|
|
|
299
386
|
* ```
|
|
300
387
|
*/
|
|
301
388
|
export function getMask(bits) {
|
|
302
|
-
|
|
303
|
-
|
|
389
|
+
anumber(bits, 'bits');
|
|
390
|
+
if (bits > 32)
|
|
391
|
+
throw new RangeError('"bits" expected <= 32, got ' + bits);
|
|
304
392
|
// JS shifts are modulo 32, so bit 32 needs an explicit full-width mask.
|
|
305
393
|
return bits === 32 ? 0xffffffff : ~(-1 << bits) >>> 0;
|
|
306
394
|
}
|
|
@@ -320,8 +408,8 @@ export const EMPTY = /* @__PURE__ */ Uint8Array.of();
|
|
|
320
408
|
* ```
|
|
321
409
|
*/
|
|
322
410
|
export function getMessage(msg, ctx = EMPTY) {
|
|
323
|
-
abytes_(msg);
|
|
324
|
-
abytes_(ctx);
|
|
411
|
+
abytes_(msg, undefined, 'msg');
|
|
412
|
+
abytes_(ctx, undefined, 'ctx');
|
|
325
413
|
if (ctx.length > 255)
|
|
326
414
|
throw new RangeError('context should be 255 bytes or less');
|
|
327
415
|
return concatBytes(new Uint8Array([0, ctx.length]), ctx, msg);
|
|
@@ -331,6 +419,19 @@ export function getMessage(msg, ctx = EMPTY) {
|
|
|
331
419
|
// SHAKE256, or another approved hash/XOF under that subtree.
|
|
332
420
|
// 06 09 60 86 48 01 65 03 04 02
|
|
333
421
|
const oidNistP = /* @__PURE__ */ Uint8Array.from([6, 9, 0x60, 0x86, 0x48, 1, 0x65, 3, 4, 2]);
|
|
422
|
+
/**
|
|
423
|
+
* Output length, in bytes, that each XOF OID under this arc denotes.
|
|
424
|
+
*
|
|
425
|
+
* Unlike a fixed hash, an XOF's OID is a promise about the digest length: RFC 8702
|
|
426
|
+
* defines id-shake128 as SHAKE128 with 256-bit output and id-shake256 as SHAKE256 with
|
|
427
|
+
* 512-bit output, and FIPS 204 / FIPS 205 use exactly those pairings for pre-hash. Both
|
|
428
|
+
* bare noble-hashes defaults are half these values, so neither can be signed under its
|
|
429
|
+
* own OID.
|
|
430
|
+
*/
|
|
431
|
+
const XOF_OID_OUTPUT_LEN = /* @__PURE__ */ (() => ({
|
|
432
|
+
'060960864801650304020b': 32, // id-shake128, SHAKE128(M, 256)
|
|
433
|
+
'060960864801650304020c': 64, // id-shake256, SHAKE256(M, 512)
|
|
434
|
+
}))();
|
|
334
435
|
/**
|
|
335
436
|
* Validates that a hash exposes a NIST hash OID and enough collision resistance.
|
|
336
437
|
* Current accepted surface is broader than the FIPS algorithm tables: any hash/XOF under the NIST
|
|
@@ -349,11 +450,29 @@ const oidNistP = /* @__PURE__ */ Uint8Array.from([6, 9, 0x60, 0x86, 0x48, 1, 0x6
|
|
|
349
450
|
* ```
|
|
350
451
|
*/
|
|
351
452
|
export function checkHash(hash, requiredStrength = 0) {
|
|
352
|
-
if (
|
|
353
|
-
throw new
|
|
453
|
+
if (typeof hash !== 'function' || typeof hash.create !== 'function')
|
|
454
|
+
throw new TypeError('"hash" expected hash function, got type=' + typeof hash);
|
|
455
|
+
ahash_(hash);
|
|
456
|
+
anumber(requiredStrength, 'requiredStrength');
|
|
457
|
+
const oid = hash.oid;
|
|
458
|
+
abytes_(oid, undefined, 'hash.oid');
|
|
459
|
+
if (!equalBytes(oid.subarray(0, 10), oidNistP))
|
|
460
|
+
throw new Error('"hash.oid" is invalid: expected NIST hash');
|
|
354
461
|
// FIPS 204 / FIPS 205 require both collision and second-preimage strength; for approved NIST
|
|
355
462
|
// hashes/XOFs under this OID subtree, the collision bound from the configured digest length is
|
|
356
463
|
// the tighter runtime check, so enforce that lower bound here.
|
|
464
|
+
// XOFs under this arc are identified by an OID that fixes their output length:
|
|
465
|
+
// FIPS 204 §5.4.1 (SHAKE128) and FIPS 205 §10.2.2 (both SHAKEs), matching RFC 8702, pair
|
|
466
|
+
// id-shake128 with SHAKE128(M, 256) and id-shake256 with SHAKE256(M, 512). getMessagePrehash embeds
|
|
467
|
+
// hash.oid beside hash(msg), so a shorter digest signs an M' that claims a length
|
|
468
|
+
// it does not have: noble-hashes' bare shake256 defaults to 32 bytes and cleared
|
|
469
|
+
// the collision bound at the 128-bit level, producing signatures a conformant
|
|
470
|
+
// verifier rejects because it recomputes 512 bits. Check the length the OID
|
|
471
|
+
// denotes rather than the generic bound.
|
|
472
|
+
const xofLen = XOF_OID_OUTPUT_LEN[bytesToHex(oid)];
|
|
473
|
+
if (xofLen !== undefined && hash.outputLen !== xofLen) {
|
|
474
|
+
throw new Error('Pre-hash XOF output length must be ' + xofLen + ' bytes for this OID, got: ' + hash.outputLen);
|
|
475
|
+
}
|
|
357
476
|
const collisionResistance = (hash.outputLen * 8) / 2;
|
|
358
477
|
if (requiredStrength > collisionResistance) {
|
|
359
478
|
throw new Error('Pre-hash security strength too low: ' +
|
|
@@ -381,11 +500,31 @@ export function checkHash(hash, requiredStrength = 0) {
|
|
|
381
500
|
* ```
|
|
382
501
|
*/
|
|
383
502
|
export function getMessagePrehash(hash, msg, ctx = EMPTY) {
|
|
384
|
-
|
|
385
|
-
abytes_(
|
|
503
|
+
checkHash(hash);
|
|
504
|
+
abytes_(msg, undefined, 'msg');
|
|
505
|
+
abytes_(ctx, undefined, 'ctx');
|
|
386
506
|
if (ctx.length > 255)
|
|
387
507
|
throw new RangeError('context should be 255 bytes or less');
|
|
388
508
|
const hashed = hash(msg);
|
|
389
509
|
return concatBytes(new Uint8Array([1, ctx.length]), ctx, hash.oid, hashed);
|
|
390
510
|
}
|
|
391
|
-
|
|
511
|
+
/**
|
|
512
|
+
* Asserts something is a string.
|
|
513
|
+
* @param value - Value to validate.
|
|
514
|
+
* @param title - Label included in thrown errors.
|
|
515
|
+
* @returns The validated string.
|
|
516
|
+
* @throws On wrong argument types. {@link TypeError}
|
|
517
|
+
* @example
|
|
518
|
+
* Validate a label string.
|
|
519
|
+
*
|
|
520
|
+
* ```ts
|
|
521
|
+
* astring('example', 'label');
|
|
522
|
+
* ```
|
|
523
|
+
*/
|
|
524
|
+
export function astring(value, title = '') {
|
|
525
|
+
if (typeof value !== 'string') {
|
|
526
|
+
const prefix = title && `"${title}" `;
|
|
527
|
+
throw new TypeError(prefix + 'expected string, got type=' + typeof value);
|
|
528
|
+
}
|
|
529
|
+
return value;
|
|
530
|
+
}
|
package/webcrypto.d.ts
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Friendly async wrappers over ML-KEM and ML-KEM-768 + X25519 from built-in WebCrypto.
|
|
3
|
+
* Private keys use the same raw seed accepted by the synchronous implementations' `keygen(seed)`;
|
|
4
|
+
* they are not expanded decapsulation keys.
|
|
5
|
+
*
|
|
6
|
+
* # WebCrypto quirks
|
|
7
|
+
*
|
|
8
|
+
* - The algorithms are experimental: a runtime can expose `encapsulateBits` and friends while
|
|
9
|
+
* implementing none of them, so support is probed with a full round-trip in `isSupported()`.
|
|
10
|
+
* - `MLKEM768-X25519` accepts `raw-seed` on import, but has no `raw-seed` / `raw-public` export.
|
|
11
|
+
* Its key bytes are read out of the JWK `priv` / `pub` members instead.
|
|
12
|
+
* - base64url is hand-rolled: scure-base's `base64urlnopad` would do, but this module must not add
|
|
13
|
+
* dependencies, and importing the synchronous implementations for four byte lengths would pull
|
|
14
|
+
* the whole lattice math into a WebCrypto-only entrypoint.
|
|
15
|
+
* @module
|
|
16
|
+
*/
|
|
17
|
+
/*! noble-post-quantum - MIT License (c) 2024 Paul Miller (paulmillr.com) */
|
|
18
|
+
import { type TArg, type TRet } from './utils.ts';
|
|
19
|
+
type MLKEMName = 'ML-KEM-512' | 'ML-KEM-768' | 'ML-KEM-1024';
|
|
20
|
+
/** Byte lengths for a WebCrypto wrapper's serialized keys and ciphertexts. */
|
|
21
|
+
type KEMLengths = {
|
|
22
|
+
/** Deterministic key-generation seed length. */
|
|
23
|
+
seed: number;
|
|
24
|
+
/**
|
|
25
|
+
* Raw seed private-key length. Note this is the *seed*, not the expanded decapsulation key:
|
|
26
|
+
* for ML-KEM the synchronous `lengths.secretKey` is much larger (1632 / 2400 / 3168 bytes), so
|
|
27
|
+
* these private keys only fit the synchronous `keygen(seed)`, never its `decapsulate(ct, sk)`.
|
|
28
|
+
*/
|
|
29
|
+
secretKey: number;
|
|
30
|
+
/** Serialized public-key length. */
|
|
31
|
+
publicKey: number;
|
|
32
|
+
/** Encapsulated ciphertext length. */
|
|
33
|
+
cipherText: number;
|
|
34
|
+
};
|
|
35
|
+
/** Async KEM interface backed by the current runtime's WebCrypto implementation. */
|
|
36
|
+
export type WebCryptoKEM = {
|
|
37
|
+
/** WebCrypto algorithm name passed to `crypto.subtle`. */
|
|
38
|
+
webCryptoName: string;
|
|
39
|
+
/** Byte lengths for this WebCrypto wrapper's serialized keys and ciphertexts. */
|
|
40
|
+
lengths: KEMLengths;
|
|
41
|
+
/**
|
|
42
|
+
* Checks whether the runtime implements the complete WebCrypto surface used by this wrapper.
|
|
43
|
+
* Probes with a real key generation and encapsulation round-trip, and memoizes the result.
|
|
44
|
+
* @returns Whether key generation, serialization, encapsulation, and decapsulation are supported.
|
|
45
|
+
*/
|
|
46
|
+
isSupported(): Promise<boolean>;
|
|
47
|
+
/**
|
|
48
|
+
* Generates a KEM key pair.
|
|
49
|
+
* @param seed - Optional raw seed for deterministic key generation.
|
|
50
|
+
* @returns Raw seed private key and serialized public key.
|
|
51
|
+
*/
|
|
52
|
+
keygen(seed?: TArg<Uint8Array>): TRet<Promise<{
|
|
53
|
+
secretKey: Uint8Array;
|
|
54
|
+
publicKey: Uint8Array;
|
|
55
|
+
}>>;
|
|
56
|
+
/**
|
|
57
|
+
* Derives a serialized public key from a raw seed private key.
|
|
58
|
+
* @param secretKey - Raw seed private key.
|
|
59
|
+
* @returns Serialized public key.
|
|
60
|
+
*/
|
|
61
|
+
getPublicKey(secretKey: TArg<Uint8Array>): TRet<Promise<Uint8Array>>;
|
|
62
|
+
/**
|
|
63
|
+
* Encapsulates a new random shared secret to a serialized public key.
|
|
64
|
+
* @param publicKey - Recipient public key.
|
|
65
|
+
* @returns Ciphertext and 32-byte shared secret.
|
|
66
|
+
*/
|
|
67
|
+
encapsulate(publicKey: TArg<Uint8Array>): TRet<Promise<{
|
|
68
|
+
cipherText: Uint8Array;
|
|
69
|
+
sharedSecret: Uint8Array;
|
|
70
|
+
}>>;
|
|
71
|
+
/**
|
|
72
|
+
* Decapsulates a ciphertext with a raw seed private key.
|
|
73
|
+
* @param cipherText - Encapsulated ciphertext bytes.
|
|
74
|
+
* @param secretKey - Private key in WebCrypto `raw-seed` format.
|
|
75
|
+
* @returns Decapsulated 32-byte shared secret.
|
|
76
|
+
*/
|
|
77
|
+
decapsulate(cipherText: TArg<Uint8Array>, secretKey: TArg<Uint8Array>): TRet<Promise<Uint8Array>>;
|
|
78
|
+
};
|
|
79
|
+
/** Async ML-KEM interface backed by the current runtime's WebCrypto implementation. */
|
|
80
|
+
export type WebCryptoMLKEM = WebCryptoKEM & {
|
|
81
|
+
webCryptoName: MLKEMName;
|
|
82
|
+
};
|
|
83
|
+
/** WebCrypto ML-KEM-512 wrapper. */
|
|
84
|
+
export declare const ml_kem512: TRet<WebCryptoMLKEM>;
|
|
85
|
+
/** WebCrypto ML-KEM-768 wrapper. */
|
|
86
|
+
export declare const ml_kem768: TRet<WebCryptoMLKEM>;
|
|
87
|
+
/** WebCrypto ML-KEM-1024 wrapper. */
|
|
88
|
+
export declare const ml_kem1024: TRet<WebCryptoMLKEM>;
|
|
89
|
+
/** WebCrypto ML-KEM-768 + X25519 (X-Wing) wrapper. */
|
|
90
|
+
export declare const ml_kem768_x25519: TRet<WebCryptoKEM>;
|
|
91
|
+
export {};
|