@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.
- package/dist/qsafe-sig.browser.min.js +61 -32
- package/index.mjs +37 -41
- package/package.json +1 -1
- package/qsafeHelper.mjs +34 -3
- package/test.mjs +6 -6
|
@@ -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
|
-
/**
|
|
3005
|
-
|
|
3006
|
-
|
|
3007
|
-
|
|
3008
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
/**
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
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
|
|
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(
|
|
69
|
-
console.assert(
|
|
70
|
-
console.assert(!
|
|
71
|
-
console.assert(!
|
|
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
|
|