@pinkparrot/qsafe-sig 0.0.7 → 0.0.9

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.
@@ -15,6 +15,14 @@
15
15
  "skipFiles": ["<node_internals>/**"],
16
16
  "program": "${workspaceFolder}/benchmark.mjs",
17
17
  "runtimeArgs": ["--inspect"]
18
+ },
19
+ {
20
+ "type": "node",
21
+ "request": "launch",
22
+ "name": "serve-front-test",
23
+ "skipFiles": ["<node_internals>/**"],
24
+ "program": "${workspaceFolder}/serve-front-test.mjs",
25
+ "runtimeArgs": ["--inspect"]
18
26
  }
19
27
  ]
20
28
  }
package/README.md CHANGED
@@ -58,7 +58,7 @@ Each `hybridKey` follow the format:
58
58
 
59
59
  The header encodes the protocol version and MAYO variant so `verify()` always knows what it's reading — no out-of-band metadata needed.
60
60
 
61
- Both keys are derived from the same master seed via **HKDF-SHA256**, using separate info tags (`qsafe-ed25519` / `qsafe-mayo`). One seed → two independent keys → one hybrid keypair.
61
+ Both keys are derived from the same master seed via **HKDF-SHA512**, using separate info tags (`qsafe-ed25519` / `qsafe-mayo`). One seed → two independent keys → one hybrid keypair.
62
62
 
63
63
  `verify()` runs Ed25519 first (pure JS, fast), and only hits WASM if that passes. Both must succeed for the signature to be valid.
64
64
 
@@ -1789,11 +1789,11 @@ var QsafeHelper = class _QsafeHelper {
1789
1789
  if (!desc) throw new Error(`Unknown variant '${variant}' for protocol version ${version}`);
1790
1790
  return desc;
1791
1791
  }
1792
- /** Derives ed25519 + mayo seeds from a master seed via HKDF-SHA256.
1792
+ /** Derives ed25519 + mayo seeds from a master seed via HKDF-SHA512.
1793
1793
  * @param {Uint8Array} masterSeed @param {number} mayoSeedSize */
1794
1794
  static deriveSeeds(masterSeed, mayoSeedSize) {
1795
- const edSeed = hkdf(sha256, masterSeed, void 0, HKDF_INFO_ED25519, ED25519_PRIV_SIZE);
1796
- const mayoSeed = hkdf(sha256, masterSeed, void 0, HKDF_INFO_MAYO, mayoSeedSize);
1795
+ const edSeed = hkdf(sha512, masterSeed, void 0, HKDF_INFO_ED25519, ED25519_PRIV_SIZE);
1796
+ const mayoSeed = hkdf(sha512, masterSeed, void 0, HKDF_INFO_MAYO, mayoSeedSize);
1797
1797
  return { edSeed, mayoSeed };
1798
1798
  }
1799
1799
  /** Build a QsafeSigner header for the given version and variant: <version(u16 BE) + variantId(u8)>
@@ -3108,6 +3108,8 @@ var QsafeSigner = class _QsafeSigner {
3108
3108
  return signer;
3109
3109
  }
3110
3110
  };
3111
+ var Qsafe = { QsafeSigner, QsafeHelper, sha256, sha512, ed25519, HEADER_SIZE, PROTOCOL_VERSIONS, CURRENT_VERSION, AVAILABLE_VERSIONS };
3112
+ var index_default = Qsafe;
3111
3113
  export {
3112
3114
  AVAILABLE_VERSIONS,
3113
3115
  CURRENT_VERSION,
@@ -3115,7 +3117,10 @@ export {
3115
3117
  PROTOCOL_VERSIONS,
3116
3118
  QsafeHelper,
3117
3119
  QsafeSigner,
3118
- ed25519
3120
+ index_default as default,
3121
+ ed25519,
3122
+ sha256,
3123
+ sha512
3119
3124
  };
3120
3125
  /*! Bundled license information:
3121
3126
 
package/index.mjs CHANGED
@@ -1,18 +1,17 @@
1
1
  // @ts-check
2
2
  import { QsafeHelper } from './qsafeHelper.mjs';
3
+ import { sha256, sha512 } from '@noble/hashes/sha2.js';
3
4
  import { ed25519 } from '@noble/curves/ed25519.js';
4
5
  import { BinaryWriter, BinaryReader } from './binary-writer-reader.mjs';
5
6
  import { PROTOCOL_VERSIONS, CURRENT_VERSION, DEFAULT_VARIANT, AVAILABLE_VERSIONS,
6
7
  ED25519_PRIV_SIZE, ED25519_PUB_SIZE, ED25519_SIG_SIZE,
7
8
  HEADER_SIZE, VARIANT_ID } from './constants.mjs';
8
9
 
9
- export { ed25519, QsafeHelper, HEADER_SIZE, PROTOCOL_VERSIONS, CURRENT_VERSION, AVAILABLE_VERSIONS };
10
-
11
10
  /**
12
11
  * @typedef {{ hybridKey : Uint8Array, secretKey: Uint8Array }} Keypair
13
12
  * @typedef {import('@pinkparrot/qsafe-mayo-wasm').MayoSigner} MayoSigner */
14
13
 
15
- export class QsafeSigner {
14
+ class QsafeSigner {
16
15
  // Shared stateless instances for verify() — one per "version:variant", loaded once.
17
16
  // These never have keypairFromSeed() called on them, so they are safe to share.
18
17
  /** @type {Record<string, MayoSigner>} */
@@ -159,4 +158,8 @@ export class QsafeSigner {
159
158
  QsafeSigner.#sharedSigners[key] = signer;
160
159
  return signer;
161
160
  }
162
- }
161
+ }
162
+
163
+ const Qsafe = { QsafeSigner, QsafeHelper, sha256, sha512, ed25519, HEADER_SIZE, PROTOCOL_VERSIONS, CURRENT_VERSION, AVAILABLE_VERSIONS };
164
+ export { QsafeSigner, QsafeHelper, sha256, sha512, ed25519, HEADER_SIZE, PROTOCOL_VERSIONS, CURRENT_VERSION, AVAILABLE_VERSIONS };
165
+ export default Qsafe;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pinkparrot/qsafe-sig",
3
- "version": "0.0.7",
3
+ "version": "0.0.9",
4
4
  "author": "PinkParrot",
5
5
  "license": "GPL-3.0",
6
6
  "description": "Combination of pre quantum and post quantum signature, designed for a smooth migration.",
package/qsafeHelper.mjs CHANGED
@@ -1,6 +1,6 @@
1
1
  // @ts-check
2
2
  import { hkdf } from '@noble/hashes/hkdf.js';
3
- import { sha256 } from '@noble/hashes/sha2.js';
3
+ import { sha512 } from '@noble/hashes/sha2.js';
4
4
  import { BinaryReader, BinaryWriter } from './binary-writer-reader.mjs';
5
5
  import { HKDF_INFO_ED25519, HKDF_INFO_MAYO, DEFAULT_VARIANT, CURRENT_VERSION,
6
6
  ED25519_SIG_SIZE, ED25519_PUB_SIZE, ED25519_PRIV_SIZE,
@@ -18,11 +18,11 @@ export class QsafeHelper {
18
18
  return desc;
19
19
  }
20
20
 
21
- /** Derives ed25519 + mayo seeds from a master seed via HKDF-SHA256.
21
+ /** Derives ed25519 + mayo seeds from a master seed via HKDF-SHA512.
22
22
  * @param {Uint8Array} masterSeed @param {number} mayoSeedSize */
23
23
  static deriveSeeds(masterSeed, mayoSeedSize) {
24
- const edSeed = hkdf(sha256, masterSeed, undefined, HKDF_INFO_ED25519, ED25519_PRIV_SIZE);
25
- const mayoSeed = hkdf(sha256, masterSeed, undefined, HKDF_INFO_MAYO, mayoSeedSize);
24
+ const edSeed = hkdf(sha512, masterSeed, undefined, HKDF_INFO_ED25519, ED25519_PRIV_SIZE);
25
+ const mayoSeed = hkdf(sha512, masterSeed, undefined, HKDF_INFO_MAYO, mayoSeedSize);
26
26
  return { edSeed, mayoSeed };
27
27
  }
28
28
 
@@ -0,0 +1,43 @@
1
+ // Simple static server for the browser test — no external deps.
2
+ // Usage: node serve-front-test.mjs [port]
3
+ import { createServer } from 'node:http';
4
+ import { readFile } from 'node:fs/promises';
5
+ import { extname, join, resolve } from 'node:path';
6
+ import { fileURLToPath } from 'node:url';
7
+
8
+ const ROOT = resolve(fileURLToPath(import.meta.url), '..');
9
+ const PORT = Number(process.argv[2]) || 3000;
10
+
11
+ const MIME = {
12
+ '.html': 'text/html',
13
+ '.mjs': 'application/javascript',
14
+ '.js': 'application/javascript',
15
+ '.wasm': 'application/wasm',
16
+ '.json': 'application/json',
17
+ };
18
+
19
+ createServer(async (req, res) => {
20
+ // Normalize path — default to test.html
21
+ const urlPath = req.url === '/' ? '/test.html' : req.url;
22
+ const filePath = join(ROOT, urlPath);
23
+
24
+ // Safety: stay inside ROOT
25
+ if (!filePath.startsWith(ROOT)) {
26
+ res.writeHead(403);
27
+ return res.end('Forbidden');
28
+ }
29
+
30
+ try {
31
+ const data = await readFile(filePath);
32
+ const mime = MIME[extname(filePath)] ?? 'application/octet-stream';
33
+ res.writeHead(200, {
34
+ 'Content-Type': mime,
35
+ 'Cross-Origin-Opener-Policy': 'same-origin',
36
+ 'Cross-Origin-Embedder-Policy': 'require-corp',
37
+ });
38
+ res.end(data);
39
+ } catch {
40
+ res.writeHead(404);
41
+ res.end(`Not found: ${urlPath}`);
42
+ }
43
+ }).listen(PORT, () => console.log(`→ http://localhost:${PORT}`));
package/test.html ADDED
@@ -0,0 +1,112 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <title>QSAFE-SIG — Browser Test</title>
6
+ <style>
7
+ body { font-family: monospace; background: #111; color: #ccc; padding: 1rem; }
8
+ .pass { color: #4f4; }
9
+ .fail { color: #f44; font-weight: bold; }
10
+ .info { color: #88f; }
11
+ .group { color: #ff8; margin-top: 1rem; }
12
+ </style>
13
+ </head>
14
+ <body>
15
+ <pre id="log">Loading…</pre>
16
+
17
+ <script type="module">
18
+ import { QsafeSigner } from './dist/qsafe-sig.browser.min.js';
19
+ import { createRandomMessage, eq } from './test-helpers.mjs';
20
+
21
+ const NB_OF_TESTS = 100;
22
+ const SEED_A = crypto.getRandomValues(new Uint8Array(32));
23
+ const SEED_B = crypto.getRandomValues(new Uint8Array(32));
24
+
25
+ // -- Logger --
26
+ const out = document.getElementById('log');
27
+ out.textContent = '';
28
+ const log = (msg, cls = '') => out.innerHTML += `<span class="${cls}">${msg}\n</span>`;
29
+ const pass = msg => log(`✓ ${msg}`, 'pass');
30
+ const fail = msg => log(`✗ ${msg}`, 'fail');
31
+ const info = msg => log(` ${msg}`, 'info');
32
+
33
+ const assert = (condition, msg) => {
34
+ console.assert(condition, msg);
35
+ if (!condition) fail(`ASSERT FAILED: ${msg}`);
36
+ };
37
+
38
+ /** @param {'mayo1'|'mayo2'} variant @param {boolean} verbose */
39
+ async function testVariant(variant, verbose) {
40
+ const msg1 = createRandomMessage(256);
41
+ const msg2 = createRandomMessage(2 ** 17);
42
+ let start = performance.now();
43
+
44
+ // -- Keypair determinism --
45
+ const signerA1 = await QsafeSigner.create(variant);
46
+ const signerA2 = await QsafeSigner.create(variant);
47
+ const kpA1 = signerA1.loadMasterKey(SEED_A);
48
+ const kpA2 = signerA2.loadMasterKey(SEED_A);
49
+ if (verbose) info(`keypair generation ~${((performance.now() - start) / 2).toFixed(2)} ms`);
50
+ assert(eq(kpA1.hybridKey, kpA2.hybridKey), `${variant} hybridKey should be deterministic`);
51
+ assert(eq(kpA1.secretKey, kpA2.secretKey), `${variant} secretKey should be deterministic`);
52
+ if (verbose) pass(`${variant} keypair determinism OK`);
53
+
54
+ // -- Different seeds → different keypairs --
55
+ const signerB = await QsafeSigner.create(variant);
56
+ const kpB = signerB.loadMasterKey(SEED_B);
57
+ assert(!eq(kpA1.hybridKey, kpB.hybridKey), `${variant} collision: same pubKey from different seeds`);
58
+ assert(!eq(kpA1.secretKey, kpB.secretKey), `${variant} collision: same secKey from different seeds`);
59
+ if (verbose) pass(`${variant} seed isolation OK`);
60
+
61
+ // -- Sign/verify roundtrip --
62
+ start = performance.now();
63
+ const sig1 = signerA1.sign(msg1);
64
+ const sig2 = signerA1.sign(msg2);
65
+ if (verbose) info(`signing ~${((performance.now() - start) / 2).toFixed(2)} ms`);
66
+
67
+ start = performance.now();
68
+ const verifier = await QsafeSigner.createFull();
69
+ assert( await verifier.verify(msg1, sig1, kpA1.hybridKey), `${variant} sig1/msg1 rejected`);
70
+ assert( await verifier.verify(msg2, sig2, kpA1.hybridKey), `${variant} sig2/msg2 rejected`);
71
+ assert(!await verifier.verify(msg2, sig1, kpA1.hybridKey), `${variant} sig1 wrongly accepts msg2`);
72
+ assert(!await verifier.verify(msg1, sig2, kpA1.hybridKey), `${variant} sig2 wrongly accepts msg1`);
73
+ if (verbose) info(`verifying ~${((performance.now() - start) / 4).toFixed(2)} ms`);
74
+ if (verbose) pass(`${variant} sign/verify roundtrip OK`);
75
+
76
+ // -- Wrong public key → rejected --
77
+ assert(!await verifier.verify(msg1, sig1, kpB.hybridKey), `${variant} wrong pubkey wrongly accepted`);
78
+ if (verbose) pass(`${variant} cross-key rejection OK`);
79
+
80
+ // -- Tampered signature → rejected --
81
+ const sigTampered = sig1.slice();
82
+ const tamperedIdx = 3 + Math.floor(Math.random() * (sig1.length - 3));
83
+ sigTampered[tamperedIdx] ^= 0xFF;
84
+ assert(!await verifier.verify(msg1, sigTampered, kpA1.hybridKey), `${variant} tampered sig wrongly accepted (idx ${tamperedIdx})`);
85
+ if (verbose) pass(`${variant} tampered sig rejection OK (flipped byte at index ${tamperedIdx})`);
86
+
87
+ // -- Tampered message → rejected --
88
+ const msgTampered = msg1.slice();
89
+ const msgTamperedIdx = Math.floor(Math.random() * msg1.length);
90
+ msgTampered[msgTamperedIdx] ^= 0xFF;
91
+ assert(!await verifier.verify(msgTampered, sig1, kpA1.hybridKey), `${variant} tampered msg wrongly accepted (idx ${msgTamperedIdx})`);
92
+ if (verbose) pass(`${variant} tampered msg rejection OK (flipped byte at index ${msgTamperedIdx})`);
93
+ }
94
+
95
+ async function run() {
96
+ try {
97
+ for (const variant of ['mayo1', 'mayo2']) {
98
+ log(`-- Testing ${variant} --`, 'group');
99
+ for (let i = 0; i < NB_OF_TESTS - 1; i++) await testVariant(variant, false);
100
+ await testVariant(variant, true);
101
+ }
102
+ log('-- TEST END --', 'group');
103
+ } catch (err) {
104
+ log(`UNCAUGHT ERROR: ${err?.message ?? err}`, 'fail');
105
+ console.error(err);
106
+ }
107
+ }
108
+
109
+ run();
110
+ </script>
111
+ </body>
112
+ </html>