@pinkparrot/qsafe-sig 0.0.2 → 0.0.4
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/.vscode/launch.json +20 -0
- package/README.md +2 -1
- package/benchmark.mjs +45 -0
- package/binary-writer-reader.mjs +20 -0
- package/constants.mjs +56 -0
- package/dist/qsafe-sig.browser.min.js +3102 -0
- package/package.json +19 -6
- package/publish.bat +22 -0
- package/qsafeHelper.mjs +28 -0
- package/test-helpers.mjs +21 -0
- package/test.mjs +83 -0
package/package.json
CHANGED
|
@@ -1,7 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pinkparrot/qsafe-sig",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.4",
|
|
4
|
+
"author": "PinkParrot",
|
|
5
|
+
"license": "GPL-3.0",
|
|
4
6
|
"description": "Combination of pre quantum and post quantum signature, designed for a smooth migration.",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"browser": "./dist/qsafe-sig.browser.min.js",
|
|
11
|
+
"default": "./index.mjs"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
5
14
|
"publishConfig": {
|
|
6
15
|
"access": "public"
|
|
7
16
|
},
|
|
@@ -11,7 +20,6 @@
|
|
|
11
20
|
"build:min": "esbuild index.mjs --bundle --outfile=dist/qsafe-sig.browser.min.js --format=esm --platform=browser --define:ENVIRONMENT_IS_NODE=false --external:node:module --external:node:fs --external:node:path --external:node:crypto --external:node:url",
|
|
12
21
|
"build": "npm run build:min"
|
|
13
22
|
},
|
|
14
|
-
"files": [],
|
|
15
23
|
"dependencies": {
|
|
16
24
|
"@noble/curves": "2.0.1",
|
|
17
25
|
"@noble/hashes": "2.0.1",
|
|
@@ -22,9 +30,14 @@
|
|
|
22
30
|
},
|
|
23
31
|
"repository": {
|
|
24
32
|
"type": "git",
|
|
25
|
-
"url": "https://github.com/Seigneur-Machiavel/qsafe-sig.git"
|
|
33
|
+
"url": "git+https://github.com/Seigneur-Machiavel/qsafe-sig.git"
|
|
26
34
|
},
|
|
27
|
-
"keywords": [
|
|
28
|
-
|
|
29
|
-
|
|
35
|
+
"keywords": [
|
|
36
|
+
"double-signature",
|
|
37
|
+
"post-quantum",
|
|
38
|
+
"mayo",
|
|
39
|
+
"signature",
|
|
40
|
+
"cryptography",
|
|
41
|
+
"pqc"
|
|
42
|
+
]
|
|
30
43
|
}
|
package/publish.bat
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
@echo off
|
|
2
|
+
echo Building qsafe-sign...
|
|
3
|
+
|
|
4
|
+
call npm run build:min
|
|
5
|
+
if errorlevel 1 (
|
|
6
|
+
echo Build failed. Aborting.
|
|
7
|
+
pause
|
|
8
|
+
exit /b 1
|
|
9
|
+
)
|
|
10
|
+
echo.
|
|
11
|
+
|
|
12
|
+
set /p PUBLISH="Publish to npm? (y/n): "
|
|
13
|
+
if /i not "%PUBLISH%"=="y" (
|
|
14
|
+
echo Build complete. Nothing published.
|
|
15
|
+
pause
|
|
16
|
+
exit /b 0
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
call npm version patch
|
|
20
|
+
call npm publish --access public
|
|
21
|
+
echo Done.
|
|
22
|
+
pause
|
package/qsafeHelper.mjs
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
import { hkdf } from '@noble/hashes/hkdf.js';
|
|
3
|
+
import { sha256 } from '@noble/hashes/sha2.js';
|
|
4
|
+
import { BinaryReader } from './binary-writer-reader.mjs';
|
|
5
|
+
import { PROTOCOL_VERSIONS, HKDF_INFO_ED25519, HKDF_INFO_MAYO,
|
|
6
|
+
ED25519_PRIV_SIZE, HEADER_SIZE, VARIANT_BY_ID } from './constants.mjs';
|
|
7
|
+
|
|
8
|
+
export class QsafeHelper {
|
|
9
|
+
/** Derives ed25519 + mayo seeds from a master seed via HKDF-SHA256.
|
|
10
|
+
* @param {Uint8Array} masterSeed @param {number} mayoSeedSize */
|
|
11
|
+
static deriveSeeds(masterSeed, mayoSeedSize) {
|
|
12
|
+
const edSeed = hkdf(sha256, masterSeed, undefined, HKDF_INFO_ED25519, ED25519_PRIV_SIZE);
|
|
13
|
+
const mayoSeed = hkdf(sha256, masterSeed, undefined, HKDF_INFO_MAYO, mayoSeedSize);
|
|
14
|
+
return { edSeed, mayoSeed };
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** Resolves version + variantId from a signature header. @param {Uint8Array} sig */
|
|
18
|
+
static parseHeader(sig) {
|
|
19
|
+
if (sig.length < HEADER_SIZE) return null;
|
|
20
|
+
const reader = new BinaryReader(sig);
|
|
21
|
+
const version = String(reader.readU16BE());
|
|
22
|
+
const variantId = reader.readByte();
|
|
23
|
+
const variant = VARIANT_BY_ID[variantId];
|
|
24
|
+
const vProto = PROTOCOL_VERSIONS[version];
|
|
25
|
+
if (!vProto || !variant || !vProto.variants[variant]) return null;
|
|
26
|
+
return { version, variant, desc: vProto.variants[variant] };
|
|
27
|
+
}
|
|
28
|
+
}
|
package/test-helpers.mjs
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
/** Generates a random Uint8Array of `len` bytes.
|
|
4
|
+
* - Uses crypto.getRandomValues in 65536-byte chunks (browser/Node API limit).
|
|
5
|
+
* @param {number} len @returns {Uint8Array} */
|
|
6
|
+
export function createRandomMessage(len) {
|
|
7
|
+
const buf = new Uint8Array(len);
|
|
8
|
+
const CHUNK = 65536;
|
|
9
|
+
for (let offset = 0; offset < len; offset += CHUNK)
|
|
10
|
+
crypto.getRandomValues(buf.subarray(offset, Math.min(offset + CHUNK, len)));
|
|
11
|
+
return buf;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** Constant-time byte equality check.
|
|
15
|
+
* @param {Uint8Array} a @param {Uint8Array} b */
|
|
16
|
+
export function eq(a, b) { // simple loop for easier debugging hover breakpoints.
|
|
17
|
+
if (a.length !== b.length) return false;
|
|
18
|
+
for (let i = 0; i < a.length; i++)
|
|
19
|
+
if (a[i] !== b[i]) return false;
|
|
20
|
+
return true;
|
|
21
|
+
}
|
package/test.mjs
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
import { QsafeSigner } from './index.mjs';
|
|
3
|
+
import { createRandomMessage, eq } from './test-helpers.mjs';
|
|
4
|
+
|
|
5
|
+
const NB_OF_TESTS = 100;
|
|
6
|
+
const msg1 = createRandomMessage(256); // 256-byte message for quick tests
|
|
7
|
+
const msg2 = createRandomMessage(2**17); // 128KB message for testing larger sizes and performance
|
|
8
|
+
|
|
9
|
+
const SEED_A = crypto.getRandomValues(new Uint8Array(32));
|
|
10
|
+
const SEED_B = crypto.getRandomValues(new Uint8Array(32));
|
|
11
|
+
|
|
12
|
+
/** @param {'mayo1'|'mayo2'} variant */
|
|
13
|
+
async function testVariant(variant, log = false) {
|
|
14
|
+
let start = performance.now();
|
|
15
|
+
|
|
16
|
+
// -- Keypair determinism --
|
|
17
|
+
const signerA1 = await QsafeSigner.create(variant);
|
|
18
|
+
const signerA2 = await QsafeSigner.create(variant);
|
|
19
|
+
const kpA1 = signerA1.loadMasterKey(SEED_A);
|
|
20
|
+
const kpA2 = signerA2.loadMasterKey(SEED_A);
|
|
21
|
+
if (log) console.log(`- keypair generation time ~${((performance.now() - start) / 2).toFixed(2)} ms`);
|
|
22
|
+
console.assert(eq(kpA1.publicKey, kpA2.publicKey), `${variant} publicKey should be deterministic`);
|
|
23
|
+
console.assert(eq(kpA1.secretKey, kpA2.secretKey), `${variant} secretKey should be deterministic`);
|
|
24
|
+
if (log) console.log(`✓ ${variant} keypair determinism OK`);
|
|
25
|
+
|
|
26
|
+
// -- Different seeds → different keypairs --
|
|
27
|
+
const signerB = await QsafeSigner.create(variant);
|
|
28
|
+
const kpB = signerB.loadMasterKey(SEED_B);
|
|
29
|
+
console.assert(!eq(kpA1.publicKey, kpB.publicKey), `${variant} collision: same pubKey from different seeds`);
|
|
30
|
+
console.assert(!eq(kpA1.secretKey, kpB.secretKey), `${variant} collision: same secKey from different seeds`);
|
|
31
|
+
if (log) console.log(`✓ ${variant} seed isolation OK`);
|
|
32
|
+
|
|
33
|
+
// -- Sign/verify roundtrip --
|
|
34
|
+
start = performance.now();
|
|
35
|
+
const sig1 = signerA1.sign(msg1);
|
|
36
|
+
const sig2 = signerA1.sign(msg2);
|
|
37
|
+
if (log) console.log(`✓ ${variant} signing OK ~${((performance.now() - start) / 2).toFixed(2)} ms`);
|
|
38
|
+
|
|
39
|
+
start = performance.now();
|
|
40
|
+
const verifier = await QsafeSigner.createFull();
|
|
41
|
+
console.assert( await verifier.verify(msg1, sig1, kpA1.publicKey), `${variant} sig1/msg1 rejected`);
|
|
42
|
+
console.assert( await verifier.verify(msg2, sig2, kpA1.publicKey), `${variant} sig2/msg2 rejected`);
|
|
43
|
+
|
|
44
|
+
console.assert(!await verifier.verify(msg2, sig1, kpA1.publicKey), `${variant} sig1 wrongly accepts msg2`);
|
|
45
|
+
console.assert(!await verifier.verify(msg1, sig2, kpA1.publicKey), `${variant} sig2 wrongly accepts msg1`);
|
|
46
|
+
if (log) console.log(`✓ ${variant} verifying OK ~${((performance.now() - start) / 4).toFixed(2)} ms`);
|
|
47
|
+
|
|
48
|
+
// -- Wrong public key → rejected --
|
|
49
|
+
console.assert(!await verifier.verify(msg1, sig1, kpB.publicKey), `${variant} wrong pubkey wrongly accepted`);
|
|
50
|
+
if (log) console.log(`✓ ${variant} cross-key rejection OK`);
|
|
51
|
+
|
|
52
|
+
// -- Tampered signature → rejected by verify(), not by checkFormat() --
|
|
53
|
+
const sigTampered = sig1.slice();
|
|
54
|
+
const tamperedIdx = 3 + Math.floor(Math.random() * (sig1.length - 3)); // random byte past the 3-byte header
|
|
55
|
+
sigTampered[tamperedIdx] ^= 0xFF;
|
|
56
|
+
console.assert(!await verifier.verify(msg1, sigTampered, kpA1.publicKey), `${variant} tampered sig wrongly accepted (flipped byte at index ${tamperedIdx})`);
|
|
57
|
+
if (log) console.log(`✓ ${variant} tampered sig rejection OK (flipped byte at index ${tamperedIdx})`);
|
|
58
|
+
|
|
59
|
+
// -- Tampered message → rejected --
|
|
60
|
+
const msgTampered = msg1.slice();
|
|
61
|
+
const msgTamperedIdx = Math.floor(Math.random() * msg1.length);
|
|
62
|
+
msgTampered[msgTamperedIdx] ^= 0xFF;
|
|
63
|
+
console.assert(!await verifier.verify(msgTampered, sig1, kpA1.publicKey), `${variant} tampered msg wrongly accepted (flipped byte at index ${msgTamperedIdx})`);
|
|
64
|
+
if (log) console.log(`✓ ${variant} tampered msg rejection OK (flipped byte at index ${msgTamperedIdx})`);
|
|
65
|
+
|
|
66
|
+
// -- checkFormat: structural check only (header + length), NOT a crypto check --
|
|
67
|
+
// A bit-flipped sig has the same length and a valid header → format is still "correct"
|
|
68
|
+
console.assert( QsafeSigner.checkFormat(sig1), `${variant} valid sig should pass checkFormat`);
|
|
69
|
+
console.assert( QsafeSigner.checkFormat(sigTampered), `${variant} tampered sig has valid format (use verify() for crypto)`);
|
|
70
|
+
console.assert(!QsafeSigner.checkFormat(sig1.slice(0, sig1.length - 1)), `${variant} truncated sig should fail checkFormat`);
|
|
71
|
+
console.assert(!QsafeSigner.checkFormat(new Uint8Array(3)), `${variant} garbage should fail checkFormat`);
|
|
72
|
+
if (log) console.log(`✓ ${variant} checkFormat OK`);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
console.log(`-- Testing mayo1 --`);
|
|
76
|
+
for (let i = 0; i < NB_OF_TESTS - 1; i++) await testVariant('mayo1');
|
|
77
|
+
await testVariant('mayo1', true); // last test: full logs.
|
|
78
|
+
|
|
79
|
+
console.log(`-- Testing mayo2 --`);
|
|
80
|
+
for (let i = 0; i < NB_OF_TESTS - 1; i++) await testVariant('mayo2');
|
|
81
|
+
await testVariant('mayo2', true); // last test: full logs.
|
|
82
|
+
|
|
83
|
+
console.log('-- TEST END --');
|