@pinkparrot/qsafe-sig 0.0.6 → 0.0.8
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 +14 -9
- package/benchmark.mjs +2 -2
- package/binary-writer-reader.mjs +1 -1
- package/constants.mjs +1 -2
- package/dist/qsafe-sig.browser.min.js +40 -38
- package/index.mjs +28 -30
- package/package.json +1 -1
- package/qsafeHelper.mjs +19 -13
- package/test.mjs +10 -18
package/README.md
CHANGED
|
@@ -31,15 +31,15 @@ const masterSeed = QsafeSigner.generateMasterKey(); // 32-byte Uint8Array
|
|
|
31
31
|
|
|
32
32
|
// Create a signer and derive a keypair from the seed
|
|
33
33
|
const signer = await QsafeSigner.create();
|
|
34
|
-
const {
|
|
34
|
+
const { hybridKey, secretKey } = signer.loadMasterKey(masterSeed);
|
|
35
35
|
|
|
36
36
|
// Sign
|
|
37
37
|
const message = new TextEncoder().encode('hello world');
|
|
38
|
-
const
|
|
38
|
+
const hybridSig = signer.sign(message);
|
|
39
39
|
|
|
40
40
|
// Verify (use createFull() for backward-compatible multi-version verification)
|
|
41
41
|
const verifier = await QsafeSigner.createFull();
|
|
42
|
-
const valid = await verifier.verify(message,
|
|
42
|
+
const valid = await verifier.verify(message, hybridSig, hybridKey);
|
|
43
43
|
console.log(valid); // true
|
|
44
44
|
```
|
|
45
45
|
|
|
@@ -48,7 +48,12 @@ console.log(valid); // true
|
|
|
48
48
|
Each `sign()` call produces a single `Uint8Array` containing:
|
|
49
49
|
|
|
50
50
|
```
|
|
51
|
-
[
|
|
51
|
+
[ ed25519 signature (64B) | MAYO signature (variant-dependent) ]
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Each `hybridKey` follow the format:
|
|
55
|
+
```
|
|
56
|
+
[ header (3B) | ed25519 pubKey | MAYO pubKey (variant-dependent) ]
|
|
52
57
|
```
|
|
53
58
|
|
|
54
59
|
The header encodes the protocol version and MAYO variant so `verify()` always knows what it's reading — no out-of-band metadata needed.
|
|
@@ -78,16 +83,16 @@ Generates a cryptographically random master seed.
|
|
|
78
83
|
|
|
79
84
|
- `size`: `16`, `24`, or `32` bytes. Default: `32`
|
|
80
85
|
|
|
81
|
-
### `
|
|
86
|
+
### `QsafeHelper.checkFormat(hybridKey, hybridSig?)` → `boolean`
|
|
82
87
|
|
|
83
|
-
Fast structural check: validates the header and byte length. **Does not perform any cryptographic verification
|
|
88
|
+
Fast structural check: validates the header and byte length. **Does not perform any cryptographic verification**.
|
|
84
89
|
|
|
85
|
-
### `signer.loadMasterKey(masterSeed)` → `{
|
|
90
|
+
### `signer.loadMasterKey(masterSeed)` → `{ hybridKey, secretKey }`
|
|
86
91
|
|
|
87
92
|
Derives and loads a keypair from the master seed. Must be called before `sign()`.
|
|
88
93
|
|
|
89
94
|
- `masterSeed`: `Uint8Array` of 16, 24, or 32 bytes
|
|
90
|
-
- Returns `{
|
|
95
|
+
- Returns `{ hybridKey: Uint8Array, secretKey: Uint8Array }`
|
|
91
96
|
|
|
92
97
|
The same instance can sign many messages after a single `loadMasterKey()` call.
|
|
93
98
|
|
|
@@ -95,7 +100,7 @@ The same instance can sign many messages after a single `loadMasterKey()` call.
|
|
|
95
100
|
|
|
96
101
|
Signs a message. Requires a prior `loadMasterKey()` call.
|
|
97
102
|
|
|
98
|
-
### `signer.verify(message,
|
|
103
|
+
### `signer.verify(message, hybridSig, hybridKey)` → `Promise<boolean>`
|
|
99
104
|
|
|
100
105
|
Verifies a hybrid signature. Lazy-loads the required WASM variant on first call, then caches it. Works across all registered protocol versions.
|
|
101
106
|
|
package/benchmark.mjs
CHANGED
|
@@ -11,7 +11,7 @@ async function chainVerify(variant) {
|
|
|
11
11
|
console.log(`\n-- Chain verify x${COUNT}: ${variant} --`);
|
|
12
12
|
|
|
13
13
|
const signer = await QsafeSigner.create(variant);
|
|
14
|
-
const {
|
|
14
|
+
const { hybridKey } = signer.loadMasterKey(SEED);
|
|
15
15
|
|
|
16
16
|
// -- Sign phase --
|
|
17
17
|
const messages = [];
|
|
@@ -30,7 +30,7 @@ async function chainVerify(variant) {
|
|
|
30
30
|
|
|
31
31
|
const t0verify = performance.now();
|
|
32
32
|
for (let i = 0; i < COUNT; i++)
|
|
33
|
-
if (!await verifier.verify(messages[i], sigs[i],
|
|
33
|
+
if (!await verifier.verify(messages[i], sigs[i], hybridKey)) failures++;
|
|
34
34
|
|
|
35
35
|
const verifyMs = performance.now() - t0verify;
|
|
36
36
|
console.log(`Verify : ${verifyMs.toFixed(2)} ms total | ~${(verifyMs / COUNT).toFixed(3)} ms/op`);
|
package/binary-writer-reader.mjs
CHANGED
package/constants.mjs
CHANGED
|
@@ -2,8 +2,7 @@
|
|
|
2
2
|
import { MayoSigner } from '@pinkparrot/qsafe-mayo-wasm';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
|
-
* @typedef {{ sigSize: number, pubKeySize: number, seedSize: number }} VariantDesc
|
|
6
|
-
* @typedef {{ publicKey: Uint8Array, secretKey: Uint8Array }} Keypair */
|
|
5
|
+
* @typedef {{ sigSize: number, pubKeySize: number, seedSize: number }} VariantDesc */
|
|
7
6
|
|
|
8
7
|
/** Versioned protocol descriptors and loaders.
|
|
9
8
|
* - Each version entry describes the crypto params for that protocol version.
|
|
@@ -1807,10 +1807,12 @@ var QsafeHelper = class _QsafeHelper {
|
|
|
1807
1807
|
w.writeByte(VARIANT_ID[variant]);
|
|
1808
1808
|
if (!writer) return w.getBytes();
|
|
1809
1809
|
}
|
|
1810
|
-
/** Resolves version + variantId from a
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1810
|
+
/** Resolves version + variantId from a hybridKey header.
|
|
1811
|
+
* - Passing the hybridKey header (3 first bytes) will produce the same result.
|
|
1812
|
+
* @param {Uint8Array} hybridKey */
|
|
1813
|
+
static parseHeader(hybridKey) {
|
|
1814
|
+
if (hybridKey.length < HEADER_SIZE) return null;
|
|
1815
|
+
const reader = new BinaryReader(hybridKey);
|
|
1814
1816
|
const version = String(reader.readU16BE());
|
|
1815
1817
|
const variantId = reader.readByte();
|
|
1816
1818
|
const variant = VARIANT_BY_ID[variantId];
|
|
@@ -1818,12 +1820,15 @@ var QsafeHelper = class _QsafeHelper {
|
|
|
1818
1820
|
if (!vProto || !variant || !vProto.variants[variant]) return null;
|
|
1819
1821
|
return { version, variant, desc: vProto.variants[variant] };
|
|
1820
1822
|
}
|
|
1821
|
-
/**
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1823
|
+
/** Quick format check for a hybridKey, without parsing the full signature.
|
|
1824
|
+
* @param {Uint8Array} hybridKey
|
|
1825
|
+
* @param {Uint8Array} [hybridSig] Optional: signature associated to the hybridKey. */
|
|
1826
|
+
static checkFormat(hybridKey, hybridSig) {
|
|
1827
|
+
const h = _QsafeHelper.parseHeader(hybridKey);
|
|
1828
|
+
if (!h) return false;
|
|
1829
|
+
if (hybridKey.length !== HEADER_SIZE + ED25519_PUB_SIZE + h.desc.pubKeySize) return false;
|
|
1830
|
+
if (hybridSig && hybridSig.length !== ED25519_SIG_SIZE + h.desc.sigSize) return false;
|
|
1831
|
+
return true;
|
|
1827
1832
|
}
|
|
1828
1833
|
};
|
|
1829
1834
|
|
|
@@ -3029,12 +3034,6 @@ var QsafeSigner = class _QsafeSigner {
|
|
|
3029
3034
|
crypto.getRandomValues(seed);
|
|
3030
3035
|
return seed;
|
|
3031
3036
|
}
|
|
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);
|
|
3037
|
-
}
|
|
3038
3037
|
/** Derives and loads a keypair from a master seed (16–32 bytes).
|
|
3039
3038
|
* - After this call, sign() is ready to use.
|
|
3040
3039
|
* - Requires a signer created with create(), not createFull().
|
|
@@ -3048,18 +3047,20 @@ var QsafeSigner = class _QsafeSigner {
|
|
|
3048
3047
|
this.#edPriv = edSeed;
|
|
3049
3048
|
const mayo = this.#mayoSigner.keypairFromSeed(mayoSeed);
|
|
3050
3049
|
if (!mayo || !this.#mayoSigner.ready) throw new Error("MAYO keypair generation failed");
|
|
3051
|
-
const
|
|
3052
|
-
|
|
3053
|
-
|
|
3054
|
-
|
|
3055
|
-
|
|
3056
|
-
|
|
3057
|
-
|
|
3058
|
-
|
|
3050
|
+
const pubWriter = new BinaryWriter(HEADER_SIZE + ED25519_PUB_SIZE + desc.pubKeySize);
|
|
3051
|
+
QsafeHelper.buildHeader(this.#variant, this.#version, pubWriter);
|
|
3052
|
+
pubWriter.writeBytes(ed25519.getPublicKey(edSeed));
|
|
3053
|
+
pubWriter.writeBytes(mayo.publicKey);
|
|
3054
|
+
const secWriter = new BinaryWriter(1 + ED25519_PRIV_SIZE + desc.seedSize);
|
|
3055
|
+
secWriter.writeByte(VARIANT_ID[this.#variant]);
|
|
3056
|
+
secWriter.writeBytes(edSeed);
|
|
3057
|
+
secWriter.writeBytes(mayo.secretKey);
|
|
3058
|
+
return { hybridKey: pubWriter.getBytes(), secretKey: secWriter.getBytes() };
|
|
3059
3059
|
}
|
|
3060
3060
|
/** Signs a message. Requires a prior loadMasterKey() call.
|
|
3061
|
-
|
|
3062
|
-
|
|
3061
|
+
* - The same instance can sign many messages without re-loading the key.
|
|
3062
|
+
* - Returned hybrid signature: [ ed25519 signature (64B) | MAYO signature (variant-dependent) ]
|
|
3063
|
+
* @param {Uint8Array} message */
|
|
3063
3064
|
sign(message) {
|
|
3064
3065
|
if (!this.#edPriv) throw new Error("No key loaded \u2014 call loadMasterKey() first");
|
|
3065
3066
|
if (!this.#mayoSigner) throw new Error("No signing instance \u2014 use QsafeSigner.create(), not createFull()");
|
|
@@ -3068,8 +3069,7 @@ var QsafeSigner = class _QsafeSigner {
|
|
|
3068
3069
|
const edSig = ed25519.sign(message, this.#edPriv);
|
|
3069
3070
|
const mayoSig = this.#mayoSigner.sign(message);
|
|
3070
3071
|
if (!mayoSig) throw new Error("MAYO sign() returned null");
|
|
3071
|
-
const writer = new BinaryWriter(
|
|
3072
|
-
QsafeHelper.buildHeader(this.#variant, this.#version, writer);
|
|
3072
|
+
const writer = new BinaryWriter(ED25519_SIG_SIZE + desc.sigSize);
|
|
3073
3073
|
writer.writeBytes(edSig);
|
|
3074
3074
|
writer.writeBytes(mayoSig);
|
|
3075
3075
|
return writer.getBytes();
|
|
@@ -3078,18 +3078,18 @@ var QsafeSigner = class _QsafeSigner {
|
|
|
3078
3078
|
* - Works with any protocol version whose descriptors are registered above.
|
|
3079
3079
|
* - Do not parallelize calls to verify(), async is justified by the lazy WASM loading. To parallelize, please use workers
|
|
3080
3080
|
* @param {Uint8Array} message
|
|
3081
|
-
* @param {Uint8Array}
|
|
3082
|
-
* @param {Uint8Array}
|
|
3083
|
-
async verify(message,
|
|
3084
|
-
const h = QsafeHelper.parseHeader(
|
|
3081
|
+
* @param {Uint8Array} hybridSig - from sign()
|
|
3082
|
+
* @param {Uint8Array} hybridKey - from loadMasterKey() */
|
|
3083
|
+
async verify(message, hybridSig, hybridKey) {
|
|
3084
|
+
const h = QsafeHelper.parseHeader(hybridKey);
|
|
3085
3085
|
if (!h) return false;
|
|
3086
|
-
if (
|
|
3087
|
-
if (
|
|
3088
|
-
const sigReader = new BinaryReader(
|
|
3089
|
-
sigReader.read(HEADER_SIZE);
|
|
3086
|
+
if (hybridSig.length !== ED25519_SIG_SIZE + h.desc.sigSize) return false;
|
|
3087
|
+
if (hybridKey.length !== HEADER_SIZE + ED25519_PUB_SIZE + h.desc.pubKeySize) return false;
|
|
3088
|
+
const sigReader = new BinaryReader(hybridSig);
|
|
3090
3089
|
const edSig = sigReader.read(ED25519_SIG_SIZE);
|
|
3091
3090
|
const mayoSig = sigReader.read(h.desc.sigSize);
|
|
3092
|
-
const pubReader = new BinaryReader(
|
|
3091
|
+
const pubReader = new BinaryReader(hybridKey);
|
|
3092
|
+
pubReader.read(HEADER_SIZE);
|
|
3093
3093
|
const edPub = pubReader.read(ED25519_PUB_SIZE);
|
|
3094
3094
|
const mayoPub = pubReader.read(h.desc.pubKeySize);
|
|
3095
3095
|
if (!ed25519.verify(edSig, message, edPub)) return false;
|
|
@@ -3108,13 +3108,15 @@ var QsafeSigner = class _QsafeSigner {
|
|
|
3108
3108
|
return signer;
|
|
3109
3109
|
}
|
|
3110
3110
|
};
|
|
3111
|
+
var Qsafe = { QsafeSigner, QsafeHelper, ed25519, PROTOCOL_VERSIONS, CURRENT_VERSION, AVAILABLE_VERSIONS };
|
|
3112
|
+
var index_default = Qsafe;
|
|
3111
3113
|
export {
|
|
3112
3114
|
AVAILABLE_VERSIONS,
|
|
3113
3115
|
CURRENT_VERSION,
|
|
3114
|
-
HEADER_SIZE,
|
|
3115
3116
|
PROTOCOL_VERSIONS,
|
|
3116
3117
|
QsafeHelper,
|
|
3117
3118
|
QsafeSigner,
|
|
3119
|
+
index_default as default,
|
|
3118
3120
|
ed25519
|
|
3119
3121
|
};
|
|
3120
3122
|
/*! Bundled license information:
|
package/index.mjs
CHANGED
|
@@ -6,13 +6,11 @@ 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, QsafeHelper, HEADER_SIZE, PROTOCOL_VERSIONS, CURRENT_VERSION, AVAILABLE_VERSIONS };
|
|
10
|
-
|
|
11
9
|
/**
|
|
12
|
-
* @typedef {{
|
|
10
|
+
* @typedef {{ hybridKey : Uint8Array, secretKey: Uint8Array }} Keypair
|
|
13
11
|
* @typedef {import('@pinkparrot/qsafe-mayo-wasm').MayoSigner} MayoSigner */
|
|
14
12
|
|
|
15
|
-
|
|
13
|
+
class QsafeSigner {
|
|
16
14
|
// Shared stateless instances for verify() — one per "version:variant", loaded once.
|
|
17
15
|
// These never have keypairFromSeed() called on them, so they are safe to share.
|
|
18
16
|
/** @type {Record<string, MayoSigner>} */
|
|
@@ -67,11 +65,6 @@ export class QsafeSigner {
|
|
|
67
65
|
return seed;
|
|
68
66
|
}
|
|
69
67
|
|
|
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); }
|
|
74
|
-
|
|
75
68
|
/** Derives and loads a keypair from a master seed (16–32 bytes).
|
|
76
69
|
* - After this call, sign() is ready to use.
|
|
77
70
|
* - Requires a signer created with create(), not createFull().
|
|
@@ -88,22 +81,24 @@ export class QsafeSigner {
|
|
|
88
81
|
const mayo = this.#mayoSigner.keypairFromSeed(mayoSeed); // stores secretKey in this.#mayoSigner
|
|
89
82
|
if (!mayo || !this.#mayoSigner.ready) throw new Error('MAYO keypair generation failed');
|
|
90
83
|
|
|
91
|
-
//
|
|
92
|
-
const
|
|
93
|
-
|
|
94
|
-
|
|
84
|
+
// hybridKey = header + ed25519_pub(32) + mayo_pub
|
|
85
|
+
const pubWriter = new BinaryWriter(HEADER_SIZE + ED25519_PUB_SIZE + desc.pubKeySize);
|
|
86
|
+
QsafeHelper.buildHeader(this.#variant, this.#version, pubWriter);
|
|
87
|
+
pubWriter.writeBytes(ed25519.getPublicKey(edSeed));
|
|
88
|
+
pubWriter.writeBytes(mayo.publicKey);
|
|
95
89
|
|
|
96
90
|
// secretKey = variantId(1) + ed25519_priv(32) + mayo_sec(seedSize)
|
|
97
|
-
const
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
91
|
+
const secWriter = new BinaryWriter(1 + ED25519_PRIV_SIZE + desc.seedSize);
|
|
92
|
+
secWriter.writeByte(VARIANT_ID[this.#variant]);
|
|
93
|
+
secWriter.writeBytes(edSeed);
|
|
94
|
+
secWriter.writeBytes(mayo.secretKey);
|
|
101
95
|
|
|
102
|
-
return {
|
|
96
|
+
return { hybridKey: pubWriter.getBytes(), secretKey: secWriter.getBytes() };
|
|
103
97
|
}
|
|
104
98
|
|
|
105
99
|
/** Signs a message. Requires a prior loadMasterKey() call.
|
|
106
100
|
* - The same instance can sign many messages without re-loading the key.
|
|
101
|
+
* - Returned hybrid signature: [ ed25519 signature (64B) | MAYO signature (variant-dependent) ]
|
|
107
102
|
* @param {Uint8Array} message */
|
|
108
103
|
sign(message) {
|
|
109
104
|
if (!this.#edPriv) throw new Error('No key loaded — call loadMasterKey() first');
|
|
@@ -115,8 +110,7 @@ export class QsafeSigner {
|
|
|
115
110
|
const mayoSig = this.#mayoSigner.sign(message);
|
|
116
111
|
if (!mayoSig) throw new Error('MAYO sign() returned null');
|
|
117
112
|
|
|
118
|
-
const writer = new BinaryWriter(
|
|
119
|
-
QsafeHelper.buildHeader(this.#variant, this.#version, writer);
|
|
113
|
+
const writer = new BinaryWriter(ED25519_SIG_SIZE + desc.sigSize);
|
|
120
114
|
writer.writeBytes(edSig);
|
|
121
115
|
writer.writeBytes(mayoSig);
|
|
122
116
|
|
|
@@ -127,20 +121,20 @@ export class QsafeSigner {
|
|
|
127
121
|
* - Works with any protocol version whose descriptors are registered above.
|
|
128
122
|
* - Do not parallelize calls to verify(), async is justified by the lazy WASM loading. To parallelize, please use workers
|
|
129
123
|
* @param {Uint8Array} message
|
|
130
|
-
* @param {Uint8Array}
|
|
131
|
-
* @param {Uint8Array}
|
|
132
|
-
async verify(message,
|
|
133
|
-
const h = QsafeHelper.parseHeader(
|
|
124
|
+
* @param {Uint8Array} hybridSig - from sign()
|
|
125
|
+
* @param {Uint8Array} hybridKey - from loadMasterKey() */
|
|
126
|
+
async verify(message, hybridSig, hybridKey) {
|
|
127
|
+
const h = QsafeHelper.parseHeader(hybridKey);
|
|
134
128
|
if (!h) return false; // invalid header or unknown version/variant
|
|
135
|
-
if (
|
|
136
|
-
if (
|
|
129
|
+
if (hybridSig.length !== ED25519_SIG_SIZE + h.desc.sigSize) return false;
|
|
130
|
+
if (hybridKey.length !== HEADER_SIZE + ED25519_PUB_SIZE + h.desc.pubKeySize) return false;
|
|
137
131
|
|
|
138
|
-
const sigReader = new BinaryReader(
|
|
139
|
-
sigReader.read(HEADER_SIZE); // skip header already parsed
|
|
132
|
+
const sigReader = new BinaryReader(hybridSig);
|
|
140
133
|
const edSig = sigReader.read(ED25519_SIG_SIZE);
|
|
141
134
|
const mayoSig = sigReader.read(h.desc.sigSize);
|
|
142
135
|
|
|
143
|
-
const pubReader = new BinaryReader(
|
|
136
|
+
const pubReader = new BinaryReader(hybridKey);
|
|
137
|
+
pubReader.read(HEADER_SIZE); // skip header already parsed
|
|
144
138
|
const edPub = pubReader.read(ED25519_PUB_SIZE);
|
|
145
139
|
const mayoPub = pubReader.read(h.desc.pubKeySize);
|
|
146
140
|
|
|
@@ -163,4 +157,8 @@ export class QsafeSigner {
|
|
|
163
157
|
QsafeSigner.#sharedSigners[key] = signer;
|
|
164
158
|
return signer;
|
|
165
159
|
}
|
|
166
|
-
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const Qsafe = { QsafeSigner, QsafeHelper, ed25519, PROTOCOL_VERSIONS, CURRENT_VERSION, AVAILABLE_VERSIONS };
|
|
163
|
+
export { QsafeSigner, QsafeHelper, ed25519, PROTOCOL_VERSIONS, CURRENT_VERSION, AVAILABLE_VERSIONS };
|
|
164
|
+
export default Qsafe;
|
package/package.json
CHANGED
package/qsafeHelper.mjs
CHANGED
|
@@ -2,8 +2,9 @@
|
|
|
2
2
|
import { hkdf } from '@noble/hashes/hkdf.js';
|
|
3
3
|
import { sha256 } from '@noble/hashes/sha2.js';
|
|
4
4
|
import { BinaryReader, BinaryWriter } from './binary-writer-reader.mjs';
|
|
5
|
-
import {
|
|
6
|
-
ED25519_SIG_SIZE,
|
|
5
|
+
import { HKDF_INFO_ED25519, HKDF_INFO_MAYO, DEFAULT_VARIANT, CURRENT_VERSION,
|
|
6
|
+
ED25519_SIG_SIZE, ED25519_PUB_SIZE, ED25519_PRIV_SIZE,
|
|
7
|
+
PROTOCOL_VERSIONS, HEADER_SIZE, VARIANT_BY_ID, VARIANT_ID } from './constants.mjs';
|
|
7
8
|
|
|
8
9
|
export class QsafeHelper {
|
|
9
10
|
/** Retrieves the descriptor for a given protocol version and variant, throwing if unknown.
|
|
@@ -37,10 +38,12 @@ export class QsafeHelper {
|
|
|
37
38
|
if (!writer) return w.getBytes();
|
|
38
39
|
}
|
|
39
40
|
|
|
40
|
-
/** Resolves version + variantId from a
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
41
|
+
/** Resolves version + variantId from a hybridKey header.
|
|
42
|
+
* - Passing the hybridKey header (3 first bytes) will produce the same result.
|
|
43
|
+
* @param {Uint8Array} hybridKey */
|
|
44
|
+
static parseHeader(hybridKey) {
|
|
45
|
+
if (hybridKey.length < HEADER_SIZE) return null;
|
|
46
|
+
const reader = new BinaryReader(hybridKey);
|
|
44
47
|
const version = String(reader.readU16BE());
|
|
45
48
|
const variantId = reader.readByte();
|
|
46
49
|
const variant = VARIANT_BY_ID[variantId];
|
|
@@ -49,11 +52,14 @@ export class QsafeHelper {
|
|
|
49
52
|
return { version, variant, desc: vProto.variants[variant] };
|
|
50
53
|
}
|
|
51
54
|
|
|
52
|
-
/**
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
55
|
+
/** Quick format check for a hybridKey, without parsing the full signature.
|
|
56
|
+
* @param {Uint8Array} hybridKey
|
|
57
|
+
* @param {Uint8Array} [hybridSig] Optional: signature associated to the hybridKey. */
|
|
58
|
+
static checkFormat(hybridKey, hybridSig) {
|
|
59
|
+
const h = QsafeHelper.parseHeader(hybridKey);
|
|
60
|
+
if (!h) return false; // invalid header or unknown version/variant
|
|
61
|
+
if (hybridKey.length !== HEADER_SIZE + ED25519_PUB_SIZE + h.desc.pubKeySize) return false;
|
|
62
|
+
if (hybridSig && hybridSig.length !== ED25519_SIG_SIZE + h.desc.sigSize) return false;
|
|
63
|
+
return true;
|
|
64
|
+
}
|
|
59
65
|
}
|
package/test.mjs
CHANGED
|
@@ -19,14 +19,14 @@ async function testVariant(variant, log = false) {
|
|
|
19
19
|
const kpA1 = signerA1.loadMasterKey(SEED_A);
|
|
20
20
|
const kpA2 = signerA2.loadMasterKey(SEED_A);
|
|
21
21
|
if (log) console.log(`- keypair generation time ~${((performance.now() - start) / 2).toFixed(2)} ms`);
|
|
22
|
-
console.assert(eq(kpA1.
|
|
22
|
+
console.assert(eq(kpA1.hybridKey, kpA2.hybridKey), `${variant} hybridKey should be deterministic`);
|
|
23
23
|
console.assert(eq(kpA1.secretKey, kpA2.secretKey), `${variant} secretKey should be deterministic`);
|
|
24
24
|
if (log) console.log(`✓ ${variant} keypair determinism OK`);
|
|
25
25
|
|
|
26
26
|
// -- Different seeds → different keypairs --
|
|
27
27
|
const signerB = await QsafeSigner.create(variant);
|
|
28
28
|
const kpB = signerB.loadMasterKey(SEED_B);
|
|
29
|
-
console.assert(!eq(kpA1.
|
|
29
|
+
console.assert(!eq(kpA1.hybridKey, kpB.hybridKey), `${variant} collision: same pubKey from different seeds`);
|
|
30
30
|
console.assert(!eq(kpA1.secretKey, kpB.secretKey), `${variant} collision: same secKey from different seeds`);
|
|
31
31
|
if (log) console.log(`✓ ${variant} seed isolation OK`);
|
|
32
32
|
|
|
@@ -38,38 +38,30 @@ async function testVariant(variant, log = false) {
|
|
|
38
38
|
|
|
39
39
|
start = performance.now();
|
|
40
40
|
const verifier = await QsafeSigner.createFull();
|
|
41
|
-
console.assert( await verifier.verify(msg1, sig1, kpA1.
|
|
42
|
-
console.assert( await verifier.verify(msg2, sig2, kpA1.
|
|
41
|
+
console.assert( await verifier.verify(msg1, sig1, kpA1.hybridKey), `${variant} sig1/msg1 rejected`);
|
|
42
|
+
console.assert( await verifier.verify(msg2, sig2, kpA1.hybridKey), `${variant} sig2/msg2 rejected`);
|
|
43
43
|
|
|
44
|
-
console.assert(!await verifier.verify(msg2, sig1, kpA1.
|
|
45
|
-
console.assert(!await verifier.verify(msg1, sig2, kpA1.
|
|
44
|
+
console.assert(!await verifier.verify(msg2, sig1, kpA1.hybridKey), `${variant} sig1 wrongly accepts msg2`);
|
|
45
|
+
console.assert(!await verifier.verify(msg1, sig2, kpA1.hybridKey), `${variant} sig2 wrongly accepts msg1`);
|
|
46
46
|
if (log) console.log(`✓ ${variant} verifying OK ~${((performance.now() - start) / 4).toFixed(2)} ms`);
|
|
47
47
|
|
|
48
48
|
// -- Wrong public key → rejected --
|
|
49
|
-
console.assert(!await verifier.verify(msg1, sig1, kpB.
|
|
49
|
+
console.assert(!await verifier.verify(msg1, sig1, kpB.hybridKey), `${variant} wrong pubkey wrongly accepted`);
|
|
50
50
|
if (log) console.log(`✓ ${variant} cross-key rejection OK`);
|
|
51
51
|
|
|
52
|
-
// -- Tampered signature → rejected by verify()
|
|
52
|
+
// -- Tampered signature → rejected by verify() --
|
|
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;
|
|
56
|
-
console.assert(!await verifier.verify(msg1, sigTampered, kpA1.
|
|
56
|
+
console.assert(!await verifier.verify(msg1, sigTampered, kpA1.hybridKey), `${variant} tampered sig wrongly accepted (flipped byte at index ${tamperedIdx})`);
|
|
57
57
|
if (log) console.log(`✓ ${variant} tampered sig rejection OK (flipped byte at index ${tamperedIdx})`);
|
|
58
58
|
|
|
59
59
|
// -- Tampered message → rejected --
|
|
60
60
|
const msgTampered = msg1.slice();
|
|
61
61
|
const msgTamperedIdx = Math.floor(Math.random() * msg1.length);
|
|
62
62
|
msgTampered[msgTamperedIdx] ^= 0xFF;
|
|
63
|
-
console.assert(!await verifier.verify(msgTampered, sig1, kpA1.
|
|
63
|
+
console.assert(!await verifier.verify(msgTampered, sig1, kpA1.hybridKey), `${variant} tampered msg wrongly accepted (flipped byte at index ${msgTamperedIdx})`);
|
|
64
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( 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
|
-
if (log) console.log(`✓ ${variant} checkFormat OK`);
|
|
73
65
|
}
|
|
74
66
|
|
|
75
67
|
console.log(`-- Testing mayo1 --`);
|