@theqrl/mldsa87 1.0.4 → 1.0.5

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.
@@ -2,6 +2,7 @@
2
2
 
3
3
  var sha3_js = require('@noble/hashes/sha3.js');
4
4
  var pkg = require('randombytes');
5
+ var utils_js = require('@noble/hashes/utils.js');
5
6
 
6
7
  const Shake128Rate = 168;
7
8
  const Shake256Rate = 136;
@@ -1030,18 +1031,36 @@ const randomBytes = pkg;
1030
1031
  const DEFAULT_CTX = new Uint8Array([0x5a, 0x4f, 0x4e, 0x44]); // "ZOND"
1031
1032
 
1032
1033
  /**
1033
- * Convert hex string to Uint8Array
1034
- * @param {string} hex - Hex-encoded string
1035
- * @returns {Uint8Array} Decoded bytes
1034
+ * Convert hex string to Uint8Array with strict validation.
1035
+ * @param {string} hex - Hex string (optional 0x prefix, even length).
1036
+ * @returns {Uint8Array} Decoded bytes.
1036
1037
  * @private
1037
1038
  */
1038
1039
  function hexToBytes(hex) {
1039
- const len = hex.length / 2;
1040
- const result = new Uint8Array(len);
1041
- for (let i = 0; i < len; i++) {
1042
- result[i] = parseInt(hex.substr(i * 2, 2), 16);
1040
+ if (typeof hex !== 'string') {
1041
+ throw new Error('message must be a hex string');
1043
1042
  }
1044
- return result;
1043
+ let clean = hex.trim();
1044
+ if (clean.startsWith('0x') || clean.startsWith('0X')) {
1045
+ clean = clean.slice(2);
1046
+ }
1047
+ if (clean.length % 2 !== 0) {
1048
+ throw new Error('hex string must have an even length');
1049
+ }
1050
+ if (!/^[0-9a-fA-F]*$/.test(clean)) {
1051
+ throw new Error('hex string contains non-hex characters');
1052
+ }
1053
+ return utils_js.hexToBytes(clean);
1054
+ }
1055
+
1056
+ function messageToBytes(message) {
1057
+ if (typeof message === 'string') {
1058
+ return hexToBytes(message);
1059
+ }
1060
+ if (message instanceof Uint8Array) {
1061
+ return message;
1062
+ }
1063
+ return null;
1045
1064
  }
1046
1065
 
1047
1066
  /**
@@ -1138,7 +1157,7 @@ function cryptoSignKeypair(passedSeed, pk, sk) {
1138
1157
  * The context parameter provides domain separation as required by FIPS 204.
1139
1158
  *
1140
1159
  * @param {Uint8Array} sig - Output buffer for signature (must be at least CryptoBytes = 4627 bytes)
1141
- * @param {string|Uint8Array} m - Message to sign (hex string or Uint8Array)
1160
+ * @param {string|Uint8Array} m - Message to sign (hex string, optional 0x prefix, or Uint8Array)
1142
1161
  * @param {Uint8Array} sk - Secret key (must be CryptoSecretKeyBytes = 4896 bytes)
1143
1162
  * @param {boolean} randomizedSigning - If true, use random nonce for hedged signing.
1144
1163
  * If false, use deterministic nonce derived from message and key.
@@ -1154,6 +1173,9 @@ function cryptoSignKeypair(passedSeed, pk, sk) {
1154
1173
  * cryptoSignSignature(sig, message, sk, false, new Uint8Array([0x01, 0x02]));
1155
1174
  */
1156
1175
  function cryptoSignSignature(sig, m, sk, randomizedSigning, ctx = DEFAULT_CTX) {
1176
+ if (!sig || sig.length < CryptoBytes) {
1177
+ throw new Error(`sig must be at least ${CryptoBytes} bytes`);
1178
+ }
1157
1179
  if (ctx.length > 255) throw new Error(`invalid context length: ${ctx.length} (max 255)`);
1158
1180
  if (sk.length !== CryptoSecretKeyBytes) {
1159
1181
  throw new Error(`invalid sk length ${sk.length} | Expected length ${CryptoSecretKeyBytes}`);
@@ -1185,8 +1207,10 @@ function cryptoSignSignature(sig, m, sk, randomizedSigning, ctx = DEFAULT_CTX) {
1185
1207
  pre[1] = ctx.length;
1186
1208
  pre.set(ctx, 2);
1187
1209
 
1188
- // Convert hex message to bytes
1189
- const mBytes = typeof m === 'string' ? hexToBytes(m) : m;
1210
+ const mBytes = messageToBytes(m);
1211
+ if (!mBytes) {
1212
+ throw new Error('message must be Uint8Array or hex string');
1213
+ }
1190
1214
 
1191
1215
  // mu = SHAKE256(tr || pre || m)
1192
1216
  const mu = sha3_js.shake256.create({}).update(tr).update(pre).update(mBytes).xof(CRHBytes);
@@ -1265,7 +1289,7 @@ function cryptoSignSignature(sig, m, sk, randomizedSigning, ctx = DEFAULT_CTX) {
1265
1289
  * This is the combined sign operation that produces a "signed message" containing
1266
1290
  * both the signature and the original message (signature || message).
1267
1291
  *
1268
- * @param {Uint8Array} msg - Message to sign
1292
+ * @param {string|Uint8Array} msg - Message to sign (hex string, optional 0x prefix, or Uint8Array)
1269
1293
  * @param {Uint8Array} sk - Secret key (must be CryptoSecretKeyBytes = 4896 bytes)
1270
1294
  * @param {boolean} randomizedSigning - If true, use random nonce; if false, deterministic
1271
1295
  * @param {Uint8Array} [ctx=DEFAULT_CTX] - Context string for domain separation (max 255 bytes).
@@ -1278,12 +1302,17 @@ function cryptoSignSignature(sig, m, sk, randomizedSigning, ctx = DEFAULT_CTX) {
1278
1302
  * // signedMsg contains: signature (4627 bytes) || message
1279
1303
  */
1280
1304
  function cryptoSign(msg, sk, randomizedSigning, ctx = DEFAULT_CTX) {
1281
- const sm = new Uint8Array(CryptoBytes + msg.length);
1282
- const mLen = msg.length;
1305
+ const msgBytes = messageToBytes(msg);
1306
+ if (!msgBytes) {
1307
+ throw new Error('message must be Uint8Array or hex string');
1308
+ }
1309
+
1310
+ const sm = new Uint8Array(CryptoBytes + msgBytes.length);
1311
+ const mLen = msgBytes.length;
1283
1312
  for (let i = 0; i < mLen; ++i) {
1284
- sm[CryptoBytes + mLen - 1 - i] = msg[mLen - 1 - i];
1313
+ sm[CryptoBytes + mLen - 1 - i] = msgBytes[mLen - 1 - i];
1285
1314
  }
1286
- const result = cryptoSignSignature(sm, msg, sk, randomizedSigning, ctx);
1315
+ const result = cryptoSignSignature(sm, msgBytes, sk, randomizedSigning, ctx);
1287
1316
 
1288
1317
  if (result !== 0) {
1289
1318
  throw new Error('failed to sign');
@@ -1298,7 +1327,7 @@ function cryptoSign(msg, sk, randomizedSigning, ctx = DEFAULT_CTX) {
1298
1327
  * The context must match the one used during signing.
1299
1328
  *
1300
1329
  * @param {Uint8Array} sig - Signature to verify (must be CryptoBytes = 4627 bytes)
1301
- * @param {string|Uint8Array} m - Message that was signed (hex string or Uint8Array)
1330
+ * @param {string|Uint8Array} m - Message that was signed (hex string, optional 0x prefix, or Uint8Array)
1302
1331
  * @param {Uint8Array} pk - Public key (must be CryptoPublicKeyBytes = 2592 bytes)
1303
1332
  * @param {Uint8Array} [ctx=DEFAULT_CTX] - Context string used during signing (max 255 bytes).
1304
1333
  * Defaults to "ZOND" for QRL compatibility.
@@ -1348,8 +1377,15 @@ function cryptoSignVerify(sig, m, pk, ctx = DEFAULT_CTX) {
1348
1377
  pre[1] = ctx.length;
1349
1378
  pre.set(ctx, 2);
1350
1379
 
1351
- // Convert hex message to bytes
1352
- const mBytes = typeof m === 'string' ? hexToBytes(m) : m;
1380
+ let mBytes;
1381
+ try {
1382
+ mBytes = messageToBytes(m);
1383
+ } catch {
1384
+ return false;
1385
+ }
1386
+ if (!mBytes) {
1387
+ return false;
1388
+ }
1353
1389
  const muFull = sha3_js.shake256.create({}).update(tr).update(pre).update(mBytes).xof(CRHBytes);
1354
1390
  mu.set(muFull);
1355
1391
 
@@ -1,5 +1,6 @@
1
1
  import { shake128, shake256 } from '@noble/hashes/sha3.js';
2
2
  import pkg from 'randombytes';
3
+ import { hexToBytes as hexToBytes$1 } from '@noble/hashes/utils.js';
3
4
 
4
5
  const Shake128Rate = 168;
5
6
  const Shake256Rate = 136;
@@ -1028,18 +1029,36 @@ const randomBytes = pkg;
1028
1029
  const DEFAULT_CTX = new Uint8Array([0x5a, 0x4f, 0x4e, 0x44]); // "ZOND"
1029
1030
 
1030
1031
  /**
1031
- * Convert hex string to Uint8Array
1032
- * @param {string} hex - Hex-encoded string
1033
- * @returns {Uint8Array} Decoded bytes
1032
+ * Convert hex string to Uint8Array with strict validation.
1033
+ * @param {string} hex - Hex string (optional 0x prefix, even length).
1034
+ * @returns {Uint8Array} Decoded bytes.
1034
1035
  * @private
1035
1036
  */
1036
1037
  function hexToBytes(hex) {
1037
- const len = hex.length / 2;
1038
- const result = new Uint8Array(len);
1039
- for (let i = 0; i < len; i++) {
1040
- result[i] = parseInt(hex.substr(i * 2, 2), 16);
1038
+ if (typeof hex !== 'string') {
1039
+ throw new Error('message must be a hex string');
1041
1040
  }
1042
- return result;
1041
+ let clean = hex.trim();
1042
+ if (clean.startsWith('0x') || clean.startsWith('0X')) {
1043
+ clean = clean.slice(2);
1044
+ }
1045
+ if (clean.length % 2 !== 0) {
1046
+ throw new Error('hex string must have an even length');
1047
+ }
1048
+ if (!/^[0-9a-fA-F]*$/.test(clean)) {
1049
+ throw new Error('hex string contains non-hex characters');
1050
+ }
1051
+ return hexToBytes$1(clean);
1052
+ }
1053
+
1054
+ function messageToBytes(message) {
1055
+ if (typeof message === 'string') {
1056
+ return hexToBytes(message);
1057
+ }
1058
+ if (message instanceof Uint8Array) {
1059
+ return message;
1060
+ }
1061
+ return null;
1043
1062
  }
1044
1063
 
1045
1064
  /**
@@ -1136,7 +1155,7 @@ function cryptoSignKeypair(passedSeed, pk, sk) {
1136
1155
  * The context parameter provides domain separation as required by FIPS 204.
1137
1156
  *
1138
1157
  * @param {Uint8Array} sig - Output buffer for signature (must be at least CryptoBytes = 4627 bytes)
1139
- * @param {string|Uint8Array} m - Message to sign (hex string or Uint8Array)
1158
+ * @param {string|Uint8Array} m - Message to sign (hex string, optional 0x prefix, or Uint8Array)
1140
1159
  * @param {Uint8Array} sk - Secret key (must be CryptoSecretKeyBytes = 4896 bytes)
1141
1160
  * @param {boolean} randomizedSigning - If true, use random nonce for hedged signing.
1142
1161
  * If false, use deterministic nonce derived from message and key.
@@ -1152,6 +1171,9 @@ function cryptoSignKeypair(passedSeed, pk, sk) {
1152
1171
  * cryptoSignSignature(sig, message, sk, false, new Uint8Array([0x01, 0x02]));
1153
1172
  */
1154
1173
  function cryptoSignSignature(sig, m, sk, randomizedSigning, ctx = DEFAULT_CTX) {
1174
+ if (!sig || sig.length < CryptoBytes) {
1175
+ throw new Error(`sig must be at least ${CryptoBytes} bytes`);
1176
+ }
1155
1177
  if (ctx.length > 255) throw new Error(`invalid context length: ${ctx.length} (max 255)`);
1156
1178
  if (sk.length !== CryptoSecretKeyBytes) {
1157
1179
  throw new Error(`invalid sk length ${sk.length} | Expected length ${CryptoSecretKeyBytes}`);
@@ -1183,8 +1205,10 @@ function cryptoSignSignature(sig, m, sk, randomizedSigning, ctx = DEFAULT_CTX) {
1183
1205
  pre[1] = ctx.length;
1184
1206
  pre.set(ctx, 2);
1185
1207
 
1186
- // Convert hex message to bytes
1187
- const mBytes = typeof m === 'string' ? hexToBytes(m) : m;
1208
+ const mBytes = messageToBytes(m);
1209
+ if (!mBytes) {
1210
+ throw new Error('message must be Uint8Array or hex string');
1211
+ }
1188
1212
 
1189
1213
  // mu = SHAKE256(tr || pre || m)
1190
1214
  const mu = shake256.create({}).update(tr).update(pre).update(mBytes).xof(CRHBytes);
@@ -1263,7 +1287,7 @@ function cryptoSignSignature(sig, m, sk, randomizedSigning, ctx = DEFAULT_CTX) {
1263
1287
  * This is the combined sign operation that produces a "signed message" containing
1264
1288
  * both the signature and the original message (signature || message).
1265
1289
  *
1266
- * @param {Uint8Array} msg - Message to sign
1290
+ * @param {string|Uint8Array} msg - Message to sign (hex string, optional 0x prefix, or Uint8Array)
1267
1291
  * @param {Uint8Array} sk - Secret key (must be CryptoSecretKeyBytes = 4896 bytes)
1268
1292
  * @param {boolean} randomizedSigning - If true, use random nonce; if false, deterministic
1269
1293
  * @param {Uint8Array} [ctx=DEFAULT_CTX] - Context string for domain separation (max 255 bytes).
@@ -1276,12 +1300,17 @@ function cryptoSignSignature(sig, m, sk, randomizedSigning, ctx = DEFAULT_CTX) {
1276
1300
  * // signedMsg contains: signature (4627 bytes) || message
1277
1301
  */
1278
1302
  function cryptoSign(msg, sk, randomizedSigning, ctx = DEFAULT_CTX) {
1279
- const sm = new Uint8Array(CryptoBytes + msg.length);
1280
- const mLen = msg.length;
1303
+ const msgBytes = messageToBytes(msg);
1304
+ if (!msgBytes) {
1305
+ throw new Error('message must be Uint8Array or hex string');
1306
+ }
1307
+
1308
+ const sm = new Uint8Array(CryptoBytes + msgBytes.length);
1309
+ const mLen = msgBytes.length;
1281
1310
  for (let i = 0; i < mLen; ++i) {
1282
- sm[CryptoBytes + mLen - 1 - i] = msg[mLen - 1 - i];
1311
+ sm[CryptoBytes + mLen - 1 - i] = msgBytes[mLen - 1 - i];
1283
1312
  }
1284
- const result = cryptoSignSignature(sm, msg, sk, randomizedSigning, ctx);
1313
+ const result = cryptoSignSignature(sm, msgBytes, sk, randomizedSigning, ctx);
1285
1314
 
1286
1315
  if (result !== 0) {
1287
1316
  throw new Error('failed to sign');
@@ -1296,7 +1325,7 @@ function cryptoSign(msg, sk, randomizedSigning, ctx = DEFAULT_CTX) {
1296
1325
  * The context must match the one used during signing.
1297
1326
  *
1298
1327
  * @param {Uint8Array} sig - Signature to verify (must be CryptoBytes = 4627 bytes)
1299
- * @param {string|Uint8Array} m - Message that was signed (hex string or Uint8Array)
1328
+ * @param {string|Uint8Array} m - Message that was signed (hex string, optional 0x prefix, or Uint8Array)
1300
1329
  * @param {Uint8Array} pk - Public key (must be CryptoPublicKeyBytes = 2592 bytes)
1301
1330
  * @param {Uint8Array} [ctx=DEFAULT_CTX] - Context string used during signing (max 255 bytes).
1302
1331
  * Defaults to "ZOND" for QRL compatibility.
@@ -1346,8 +1375,15 @@ function cryptoSignVerify(sig, m, pk, ctx = DEFAULT_CTX) {
1346
1375
  pre[1] = ctx.length;
1347
1376
  pre.set(ctx, 2);
1348
1377
 
1349
- // Convert hex message to bytes
1350
- const mBytes = typeof m === 'string' ? hexToBytes(m) : m;
1378
+ let mBytes;
1379
+ try {
1380
+ mBytes = messageToBytes(m);
1381
+ } catch {
1382
+ return false;
1383
+ }
1384
+ if (!mBytes) {
1385
+ return false;
1386
+ }
1351
1387
  const muFull = shake256.create({}).update(tr).update(pre).update(mBytes).xof(CRHBytes);
1352
1388
  mu.set(muFull);
1353
1389
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@theqrl/mldsa87",
3
- "version": "1.0.4",
3
+ "version": "1.0.5",
4
4
  "description": "ML-DSA-87 cryptography",
5
5
  "keywords": [
6
6
  "ml-dsa",
@@ -55,7 +55,7 @@
55
55
  "eslint-config-prettier": "^10.1.8",
56
56
  "eslint-plugin-import-x": "^4.15.0",
57
57
  "eslint-plugin-prettier": "^5.5.4",
58
- "globals": "^16.2.0",
58
+ "globals": "^17.0.0",
59
59
  "mocha": "^11.7.5",
60
60
  "prettier": "^3.7.4",
61
61
  "rollup": "^4.55.1"
package/src/index.d.ts CHANGED
@@ -58,7 +58,7 @@ export function cryptoSignKeypair(
58
58
  /**
59
59
  * Create a signature for a message with optional context
60
60
  * @param sig - Output buffer for signature (must be CryptoBytes length minimum)
61
- * @param m - Message to sign (hex-encoded string)
61
+ * @param m - Message to sign (hex string or Uint8Array)
62
62
  * @param sk - Secret key
63
63
  * @param randomizedSigning - If true, use random nonce; if false, deterministic
64
64
  * @param ctx - Optional context string (max 255 bytes, defaults to "ZOND")
@@ -67,7 +67,7 @@ export function cryptoSignKeypair(
67
67
  */
68
68
  export function cryptoSignSignature(
69
69
  sig: Uint8Array,
70
- m: string,
70
+ m: Uint8Array | string,
71
71
  sk: Uint8Array,
72
72
  randomizedSigning: boolean,
73
73
  ctx?: Uint8Array
@@ -83,7 +83,7 @@ export function cryptoSignSignature(
83
83
  * @throws Error if signing fails
84
84
  */
85
85
  export function cryptoSign(
86
- msg: Uint8Array,
86
+ msg: Uint8Array | string,
87
87
  sk: Uint8Array,
88
88
  randomizedSigning: boolean,
89
89
  ctx?: Uint8Array
@@ -92,14 +92,14 @@ export function cryptoSign(
92
92
  /**
93
93
  * Verify a signature with optional context
94
94
  * @param sig - Signature to verify
95
- * @param m - Message that was signed (hex-encoded string)
95
+ * @param m - Message that was signed (hex string or Uint8Array)
96
96
  * @param pk - Public key
97
97
  * @param ctx - Optional context string (max 255 bytes, defaults to "ZOND")
98
98
  * @returns true if signature is valid, false otherwise
99
99
  */
100
100
  export function cryptoSignVerify(
101
101
  sig: Uint8Array,
102
- m: string,
102
+ m: Uint8Array | string,
103
103
  pk: Uint8Array,
104
104
  ctx?: Uint8Array
105
105
  ): boolean;