@pinkparrot/qsafe-sig 0.0.4 → 0.0.6

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.
@@ -1778,7 +1778,17 @@ var VARIANT_ID = { "mayo1": 1, "mayo2": 2 };
1778
1778
  var VARIANT_BY_ID = { 1: "mayo1", 2: "mayo2" };
1779
1779
 
1780
1780
  // qsafeHelper.mjs
1781
- var QsafeHelper = class {
1781
+ var QsafeHelper = class _QsafeHelper {
1782
+ /** Retrieves the descriptor for a given protocol version and variant, throwing if unknown.
1783
+ * @param {'mayo1' | 'mayo2'} [variant] defaults to DEFAULT_VARIANT
1784
+ * @param {string} [version] defaults to CURRENT_VERSION */
1785
+ static getVariantDescriptor(variant = DEFAULT_VARIANT, version = CURRENT_VERSION) {
1786
+ const vProto = PROTOCOL_VERSIONS[version];
1787
+ if (!vProto) throw new Error(`Unknown protocol version: ${version}`);
1788
+ const desc = vProto.variants[variant];
1789
+ if (!desc) throw new Error(`Unknown variant '${variant}' for protocol version ${version}`);
1790
+ return desc;
1791
+ }
1782
1792
  /** Derives ed25519 + mayo seeds from a master seed via HKDF-SHA256.
1783
1793
  * @param {Uint8Array} masterSeed @param {number} mayoSeedSize */
1784
1794
  static deriveSeeds(masterSeed, mayoSeedSize) {
@@ -1786,6 +1796,17 @@ var QsafeHelper = class {
1786
1796
  const mayoSeed = hkdf(sha256, masterSeed, void 0, HKDF_INFO_MAYO, mayoSeedSize);
1787
1797
  return { edSeed, mayoSeed };
1788
1798
  }
1799
+ /** Build a QsafeSigner header for the given version and variant: <version(u16 BE) + variantId(u8)>
1800
+ * - Returned as a Uint8Array or write directly at cursor position if a BinaryWriter is provided.
1801
+ * @param {'mayo1' | 'mayo2'} [variant] defaults to DEFAULT_VARIANT
1802
+ * @param {string} [version] defaults to CURRENT_VERSION
1803
+ * @param {BinaryWriter} [writer] - Optional pre-allocated writer */
1804
+ static buildHeader(variant = DEFAULT_VARIANT, version = CURRENT_VERSION, writer) {
1805
+ const w = writer || new BinaryWriter(HEADER_SIZE);
1806
+ w.writeU16BE(Number(version));
1807
+ w.writeByte(VARIANT_ID[variant]);
1808
+ if (!writer) return w.getBytes();
1809
+ }
1789
1810
  /** Resolves version + variantId from a signature header. @param {Uint8Array} sig */
1790
1811
  static parseHeader(sig) {
1791
1812
  if (sig.length < HEADER_SIZE) return null;
@@ -1797,6 +1818,13 @@ var QsafeHelper = class {
1797
1818
  if (!vProto || !variant || !vProto.variants[variant]) return null;
1798
1819
  return { version, variant, desc: vProto.variants[variant] };
1799
1820
  }
1821
+ /** Checks that a signature buffer has a valid header and correct byte length.
1822
+ * - Zero crypto — safe to call as a fast pre-filter. @param {Uint8Array} signature */
1823
+ static checkSignatureFormat(signature) {
1824
+ const h = _QsafeHelper.parseHeader(signature);
1825
+ if (h) return signature.length === HEADER_SIZE + ED25519_SIG_SIZE + h.desc.sigSize;
1826
+ else return false;
1827
+ }
1800
1828
  };
1801
1829
 
1802
1830
  // node_modules/@noble/curves/utils.js
@@ -3001,31 +3029,11 @@ var QsafeSigner = class _QsafeSigner {
3001
3029
  crypto.getRandomValues(seed);
3002
3030
  return seed;
3003
3031
  }
3004
- /** Checks that a signature buffer has a valid header and correct byte length.
3005
- * - Zero crypto safe to call as a fast pre-filter. @param {Uint8Array} signature */
3006
- static checkFormat(signature) {
3007
- const h = QsafeHelper.parseHeader(signature);
3008
- if (h) return signature.length === HEADER_SIZE + ED25519_SIG_SIZE + h.desc.sigSize;
3009
- else return false;
3010
- }
3011
- /** Verifies a hybrid signature. Lazy-loads the required WASM variant if not already cached.
3012
- * - Works with any protocol version whose descriptors are registered above.
3013
- * @param {Uint8Array} message
3014
- * @param {Uint8Array} signature - from sign()
3015
- * @param {Uint8Array} publicKey - from loadMasterKey() */
3016
- async verify(message, signature, publicKey) {
3017
- const h = QsafeHelper.parseHeader(signature);
3018
- if (!h || !_QsafeSigner.checkFormat(signature)) return false;
3019
- const sigReader = new BinaryReader(signature);
3020
- sigReader.read(HEADER_SIZE);
3021
- const edSig = sigReader.read(ED25519_SIG_SIZE);
3022
- const mayoSig = sigReader.read(h.desc.sigSize);
3023
- const pubReader = new BinaryReader(publicKey);
3024
- const edPub = pubReader.read(ED25519_PUB_SIZE);
3025
- const mayoPub = pubReader.read(h.desc.pubKeySize);
3026
- if (!ed25519.verify(edSig, message, edPub)) return false;
3027
- const signer = await this.#ensureShared(h.version, h.variant);
3028
- return signer.verify(message, mayoSig, mayoPub);
3032
+ /** Parses a signature header and resolves its protocol version and variant.
3033
+ * - Returns null if the header is invalid or references an unknown version/variant.
3034
+ * @param {Uint8Array} headerOrSignature */
3035
+ static parseHeader(headerOrSignature) {
3036
+ return QsafeHelper.parseHeader(headerOrSignature);
3029
3037
  }
3030
3038
  /** Derives and loads a keypair from a master seed (16–32 bytes).
3031
3039
  * - After this call, sign() is ready to use.
@@ -3035,8 +3043,7 @@ var QsafeSigner = class _QsafeSigner {
3035
3043
  const isValidSize = masterSeed instanceof Uint8Array && (masterSeed.length === 16 || masterSeed.length === 24 || masterSeed.length === 32);
3036
3044
  if (!isValidSize) throw new TypeError("masterSeed must be a Uint8Array of 16, 24 or 32 bytes");
3037
3045
  if (!this.#mayoSigner) throw new Error("No signing instance \u2014 use QsafeSigner.create(), not createFull()");
3038
- const proto = PROTOCOL_VERSIONS[this.#version];
3039
- const desc = proto.variants[this.#variant];
3046
+ const desc = QsafeHelper.getVariantDescriptor(this.#variant, this.#version);
3040
3047
  const { edSeed, mayoSeed } = QsafeHelper.deriveSeeds(masterSeed, desc.seedSize);
3041
3048
  this.#edPriv = edSeed;
3042
3049
  const mayo = this.#mayoSigner.keypairFromSeed(mayoSeed);
@@ -3057,18 +3064,38 @@ var QsafeSigner = class _QsafeSigner {
3057
3064
  if (!this.#edPriv) throw new Error("No key loaded \u2014 call loadMasterKey() first");
3058
3065
  if (!this.#mayoSigner) throw new Error("No signing instance \u2014 use QsafeSigner.create(), not createFull()");
3059
3066
  if (!this.#mayoSigner.ready) throw new Error("MAYO signer not ready \u2014 was create() called?");
3060
- const proto = PROTOCOL_VERSIONS[this.#version];
3061
- const desc = proto.variants[this.#variant];
3067
+ const desc = QsafeHelper.getVariantDescriptor(this.#variant, this.#version);
3062
3068
  const edSig = ed25519.sign(message, this.#edPriv);
3063
3069
  const mayoSig = this.#mayoSigner.sign(message);
3064
3070
  if (!mayoSig) throw new Error("MAYO sign() returned null");
3065
3071
  const writer = new BinaryWriter(HEADER_SIZE + ED25519_SIG_SIZE + desc.sigSize);
3066
- writer.writeU16BE(Number(this.#version));
3067
- writer.writeByte(VARIANT_ID[this.#variant]);
3072
+ QsafeHelper.buildHeader(this.#variant, this.#version, writer);
3068
3073
  writer.writeBytes(edSig);
3069
3074
  writer.writeBytes(mayoSig);
3070
3075
  return writer.getBytes();
3071
3076
  }
3077
+ /** Verifies a hybrid signature. Lazy-loads the required WASM variant if not already cached.
3078
+ * - Works with any protocol version whose descriptors are registered above.
3079
+ * - Do not parallelize calls to verify(), async is justified by the lazy WASM loading. To parallelize, please use workers
3080
+ * @param {Uint8Array} message
3081
+ * @param {Uint8Array} signature - from sign()
3082
+ * @param {Uint8Array} publicKey - from loadMasterKey() */
3083
+ async verify(message, signature, publicKey) {
3084
+ const h = QsafeHelper.parseHeader(signature);
3085
+ if (!h) return false;
3086
+ if (signature.length !== HEADER_SIZE + ED25519_SIG_SIZE + h.desc.sigSize) return false;
3087
+ if (publicKey.length !== ED25519_PUB_SIZE + h.desc.pubKeySize) return false;
3088
+ const sigReader = new BinaryReader(signature);
3089
+ sigReader.read(HEADER_SIZE);
3090
+ const edSig = sigReader.read(ED25519_SIG_SIZE);
3091
+ const mayoSig = sigReader.read(h.desc.sigSize);
3092
+ const pubReader = new BinaryReader(publicKey);
3093
+ const edPub = pubReader.read(ED25519_PUB_SIZE);
3094
+ const mayoPub = pubReader.read(h.desc.pubKeySize);
3095
+ if (!ed25519.verify(edSig, message, edPub)) return false;
3096
+ const signer = await this.#ensureShared(h.version, h.variant);
3097
+ return signer.verify(message, mayoSig, mayoPub);
3098
+ }
3072
3099
  /** Loads and caches a shared MayoSigner for version+variant. Idempotent.
3073
3100
  * These instances are ONLY used for verify() — keypairFromSeed() is never called on them.
3074
3101
  * @param {string} version @param {'mayo1' | 'mayo2' | string} variant */
@@ -3084,7 +3111,9 @@ var QsafeSigner = class _QsafeSigner {
3084
3111
  export {
3085
3112
  AVAILABLE_VERSIONS,
3086
3113
  CURRENT_VERSION,
3114
+ HEADER_SIZE,
3087
3115
  PROTOCOL_VERSIONS,
3116
+ QsafeHelper,
3088
3117
  QsafeSigner,
3089
3118
  ed25519
3090
3119
  };
package/index.mjs CHANGED
@@ -6,7 +6,7 @@ import { PROTOCOL_VERSIONS, CURRENT_VERSION, DEFAULT_VARIANT, AVAILABLE_VERSIONS
6
6
  ED25519_PRIV_SIZE, ED25519_PUB_SIZE, ED25519_SIG_SIZE,
7
7
  HEADER_SIZE, VARIANT_ID } from './constants.mjs';
8
8
 
9
- export { ed25519, PROTOCOL_VERSIONS, CURRENT_VERSION, AVAILABLE_VERSIONS };
9
+ export { ed25519, QsafeHelper, HEADER_SIZE, PROTOCOL_VERSIONS, CURRENT_VERSION, AVAILABLE_VERSIONS };
10
10
 
11
11
  /**
12
12
  * @typedef {{ publicKey: Uint8Array, secretKey: Uint8Array }} Keypair
@@ -67,39 +67,10 @@ export class QsafeSigner {
67
67
  return seed;
68
68
  }
69
69
 
70
- /** Checks that a signature buffer has a valid header and correct byte length.
71
- * - Zero crypto safe to call as a fast pre-filter. @param {Uint8Array} signature */
72
- static checkFormat(signature) {
73
- const h = QsafeHelper.parseHeader(signature);
74
- if (h) return signature.length === HEADER_SIZE + ED25519_SIG_SIZE + h.desc.sigSize;
75
- else return false;
76
- }
77
-
78
- /** Verifies a hybrid signature. Lazy-loads the required WASM variant if not already cached.
79
- * - Works with any protocol version whose descriptors are registered above.
80
- * @param {Uint8Array} message
81
- * @param {Uint8Array} signature - from sign()
82
- * @param {Uint8Array} publicKey - from loadMasterKey() */
83
- async verify(message, signature, publicKey) {
84
- const h = QsafeHelper.parseHeader(signature);
85
- if (!h || !QsafeSigner.checkFormat(signature)) return false;
86
-
87
- const sigReader = new BinaryReader(signature);
88
- sigReader.read(HEADER_SIZE); // skip header already parsed
89
- const edSig = sigReader.read(ED25519_SIG_SIZE);
90
- const mayoSig = sigReader.read(h.desc.sigSize);
91
-
92
- const pubReader = new BinaryReader(publicKey);
93
- const edPub = pubReader.read(ED25519_PUB_SIZE);
94
- const mayoPub = pubReader.read(h.desc.pubKeySize);
95
-
96
- // Fast path: ed25519 first (pure JS, no WASM)
97
- if (!ed25519.verify(edSig, message, edPub)) return false;
98
-
99
- // Lazy-load the shared signer for this version+variant if not already cached
100
- const signer = await this.#ensureShared(h.version, h.variant);
101
- return signer.verify(message, mayoSig, mayoPub);
102
- }
70
+ /** Parses a signature header and resolves its protocol version and variant.
71
+ * - Returns null if the header is invalid or references an unknown version/variant.
72
+ * @param {Uint8Array} headerOrSignature */
73
+ static parseHeader(headerOrSignature) { return QsafeHelper.parseHeader(headerOrSignature); }
103
74
 
104
75
  /** Derives and loads a keypair from a master seed (16–32 bytes).
105
76
  * - After this call, sign() is ready to use.
@@ -110,8 +81,7 @@ export class QsafeSigner {
110
81
  if (!isValidSize) throw new TypeError('masterSeed must be a Uint8Array of 16, 24 or 32 bytes');
111
82
  if (!this.#mayoSigner) throw new Error('No signing instance — use QsafeSigner.create(), not createFull()');
112
83
 
113
- const proto = PROTOCOL_VERSIONS[this.#version];
114
- const desc = proto.variants[this.#variant];
84
+ const desc = QsafeHelper.getVariantDescriptor(this.#variant, this.#version);
115
85
  const { edSeed, mayoSeed } = QsafeHelper.deriveSeeds(masterSeed, desc.seedSize);
116
86
 
117
87
  this.#edPriv = edSeed;
@@ -140,22 +110,48 @@ export class QsafeSigner {
140
110
  if (!this.#mayoSigner) throw new Error('No signing instance — use QsafeSigner.create(), not createFull()');
141
111
  if (!this.#mayoSigner.ready) throw new Error('MAYO signer not ready — was create() called?');
142
112
 
143
- const proto = PROTOCOL_VERSIONS[this.#version];
144
- const desc = proto.variants[this.#variant];
113
+ const desc = QsafeHelper.getVariantDescriptor(this.#variant, this.#version);
145
114
  const edSig = ed25519.sign(message, this.#edPriv);
146
115
  const mayoSig = this.#mayoSigner.sign(message);
147
116
  if (!mayoSig) throw new Error('MAYO sign() returned null');
148
117
 
149
- // header: version(u16 BE) + variantId(u8)
150
118
  const writer = new BinaryWriter(HEADER_SIZE + ED25519_SIG_SIZE + desc.sigSize);
151
- writer.writeU16BE(Number(this.#version));
152
- writer.writeByte(VARIANT_ID[this.#variant]);
119
+ QsafeHelper.buildHeader(this.#variant, this.#version, writer);
153
120
  writer.writeBytes(edSig);
154
121
  writer.writeBytes(mayoSig);
155
122
 
156
123
  return writer.getBytes();
157
124
  }
158
125
 
126
+ /** Verifies a hybrid signature. Lazy-loads the required WASM variant if not already cached.
127
+ * - Works with any protocol version whose descriptors are registered above.
128
+ * - Do not parallelize calls to verify(), async is justified by the lazy WASM loading. To parallelize, please use workers
129
+ * @param {Uint8Array} message
130
+ * @param {Uint8Array} signature - from sign()
131
+ * @param {Uint8Array} publicKey - from loadMasterKey() */
132
+ async verify(message, signature, publicKey) {
133
+ const h = QsafeHelper.parseHeader(signature);
134
+ if (!h) return false; // invalid header or unknown version/variant
135
+ if (signature.length !== HEADER_SIZE + ED25519_SIG_SIZE + h.desc.sigSize) return false;
136
+ if (publicKey.length !== ED25519_PUB_SIZE + h.desc.pubKeySize) return false;
137
+
138
+ const sigReader = new BinaryReader(signature);
139
+ sigReader.read(HEADER_SIZE); // skip header already parsed
140
+ const edSig = sigReader.read(ED25519_SIG_SIZE);
141
+ const mayoSig = sigReader.read(h.desc.sigSize);
142
+
143
+ const pubReader = new BinaryReader(publicKey);
144
+ const edPub = pubReader.read(ED25519_PUB_SIZE);
145
+ const mayoPub = pubReader.read(h.desc.pubKeySize);
146
+
147
+ // Fast path: ed25519 first (pure JS, no WASM)
148
+ if (!ed25519.verify(edSig, message, edPub)) return false;
149
+
150
+ // Lazy-load the shared signer for this version+variant if not already cached
151
+ const signer = await this.#ensureShared(h.version, h.variant);
152
+ return signer.verify(message, mayoSig, mayoPub);
153
+ }
154
+
159
155
  /** Loads and caches a shared MayoSigner for version+variant. Idempotent.
160
156
  * These instances are ONLY used for verify() — keypairFromSeed() is never called on them.
161
157
  * @param {string} version @param {'mayo1' | 'mayo2' | string} variant */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pinkparrot/qsafe-sig",
3
- "version": "0.0.4",
3
+ "version": "0.0.6",
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,11 +1,22 @@
1
1
  // @ts-check
2
2
  import { hkdf } from '@noble/hashes/hkdf.js';
3
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';
4
+ import { BinaryReader, BinaryWriter } from './binary-writer-reader.mjs';
5
+ import { PROTOCOL_VERSIONS, HKDF_INFO_ED25519, HKDF_INFO_MAYO, DEFAULT_VARIANT, CURRENT_VERSION,
6
+ ED25519_SIG_SIZE, ED25519_PRIV_SIZE, HEADER_SIZE, VARIANT_BY_ID, VARIANT_ID } from './constants.mjs';
7
7
 
8
8
  export class QsafeHelper {
9
+ /** Retrieves the descriptor for a given protocol version and variant, throwing if unknown.
10
+ * @param {'mayo1' | 'mayo2'} [variant] defaults to DEFAULT_VARIANT
11
+ * @param {string} [version] defaults to CURRENT_VERSION */
12
+ static getVariantDescriptor(variant = DEFAULT_VARIANT, version = CURRENT_VERSION) {
13
+ const vProto = PROTOCOL_VERSIONS[version];
14
+ if (!vProto) throw new Error(`Unknown protocol version: ${version}`);
15
+ const desc = vProto.variants[variant];
16
+ if (!desc) throw new Error(`Unknown variant '${variant}' for protocol version ${version}`);
17
+ return desc;
18
+ }
19
+
9
20
  /** Derives ed25519 + mayo seeds from a master seed via HKDF-SHA256.
10
21
  * @param {Uint8Array} masterSeed @param {number} mayoSeedSize */
11
22
  static deriveSeeds(masterSeed, mayoSeedSize) {
@@ -14,6 +25,18 @@ export class QsafeHelper {
14
25
  return { edSeed, mayoSeed };
15
26
  }
16
27
 
28
+ /** Build a QsafeSigner header for the given version and variant: <version(u16 BE) + variantId(u8)>
29
+ * - Returned as a Uint8Array or write directly at cursor position if a BinaryWriter is provided.
30
+ * @param {'mayo1' | 'mayo2'} [variant] defaults to DEFAULT_VARIANT
31
+ * @param {string} [version] defaults to CURRENT_VERSION
32
+ * @param {BinaryWriter} [writer] - Optional pre-allocated writer */
33
+ static buildHeader(variant = DEFAULT_VARIANT, version = CURRENT_VERSION, writer) {
34
+ const w = writer || new BinaryWriter(HEADER_SIZE);
35
+ w.writeU16BE(Number(version));
36
+ w.writeByte(VARIANT_ID[variant]);
37
+ if (!writer) return w.getBytes();
38
+ }
39
+
17
40
  /** Resolves version + variantId from a signature header. @param {Uint8Array} sig */
18
41
  static parseHeader(sig) {
19
42
  if (sig.length < HEADER_SIZE) return null;
@@ -25,4 +48,12 @@ export class QsafeHelper {
25
48
  if (!vProto || !variant || !vProto.variants[variant]) return null;
26
49
  return { version, variant, desc: vProto.variants[variant] };
27
50
  }
51
+
52
+ /** Checks that a signature buffer has a valid header and correct byte length.
53
+ * - Zero crypto — safe to call as a fast pre-filter. @param {Uint8Array} signature */
54
+ static checkSignatureFormat(signature) {
55
+ const h = QsafeHelper.parseHeader(signature);
56
+ if (h) return signature.length === HEADER_SIZE + ED25519_SIG_SIZE + h.desc.sigSize;
57
+ else return false;
58
+ }
28
59
  }
package/test.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  // @ts-check
2
- import { QsafeSigner } from './index.mjs';
2
+ import { QsafeHelper, QsafeSigner } from './index.mjs';
3
3
  import { createRandomMessage, eq } from './test-helpers.mjs';
4
4
 
5
5
  const NB_OF_TESTS = 100;
@@ -49,7 +49,7 @@ async function testVariant(variant, log = false) {
49
49
  console.assert(!await verifier.verify(msg1, sig1, kpB.publicKey), `${variant} wrong pubkey wrongly accepted`);
50
50
  if (log) console.log(`✓ ${variant} cross-key rejection OK`);
51
51
 
52
- // -- Tampered signature → rejected by verify(), not by checkFormat() --
52
+ // -- Tampered signature → rejected by verify(), not by checkSignatureFormat() --
53
53
  const sigTampered = sig1.slice();
54
54
  const tamperedIdx = 3 + Math.floor(Math.random() * (sig1.length - 3)); // random byte past the 3-byte header
55
55
  sigTampered[tamperedIdx] ^= 0xFF;
@@ -65,10 +65,10 @@ async function testVariant(variant, log = false) {
65
65
 
66
66
  // -- checkFormat: structural check only (header + length), NOT a crypto check --
67
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`);
68
+ console.assert( QsafeHelper.checkSignatureFormat(sig1), `${variant} valid sig should pass checkFormat`);
69
+ console.assert( QsafeHelper.checkSignatureFormat(sigTampered), `${variant} tampered sig has valid format (use verify() for crypto)`);
70
+ console.assert(!QsafeHelper.checkSignatureFormat(sig1.slice(0, sig1.length - 1)), `${variant} truncated sig should fail checkFormat`);
71
+ console.assert(!QsafeHelper.checkSignatureFormat(new Uint8Array(3)), `${variant} garbage should fail checkFormat`);
72
72
  if (log) console.log(`✓ ${variant} checkFormat OK`);
73
73
  }
74
74