hd-wallet-wasm 2.0.19 → 2.0.21

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hd-wallet-wasm",
3
- "version": "2.0.19",
3
+ "version": "2.0.21",
4
4
  "description": "Comprehensive HD Wallet implementation in WebAssembly - BIP-32/39/44, multi-curve, multi-chain support",
5
5
  "type": "module",
6
6
  "main": "src/index.mjs",
@@ -59,48 +59,178 @@ export function buildCanonicalPayload({
59
59
  // EPM Content Signing
60
60
  // =============================================================================
61
61
 
62
+ // EntityType / KeyType enum labels (FlatBuffer order). The in-module verifier
63
+ // emits these names, so we must too.
64
+ const EPM_ENTITY_TYPE_NAMES = ['User', 'Node'];
65
+ const EPM_KEY_TYPE_NAMES = ['Signing', 'Encryption'];
66
+
67
+ // Whitespace set trimmed by the Go/C++ canonicalizer: space, tab, NL, CR, VT, FF.
68
+ // (Deliberately NOT JS \s, which also strips NBSP/U+2028/etc. and would diverge.)
69
+ function epmTrim(value) {
70
+ if (typeof value !== 'string') return '';
71
+ return value.replace(/^[ \t\n\r\x0b\f]+/, '').replace(/[ \t\n\r\x0b\f]+$/, '');
72
+ }
73
+
74
+ // Go addBytesString: trim, omit when empty.
75
+ function epmAddStr(obj, key, value) {
76
+ const t = epmTrim(value);
77
+ if (t !== '') obj[key] = t;
78
+ }
79
+
80
+ // Trimmed non-empty strings -> array; attach only when non-empty.
81
+ function epmAddStrArray(obj, key, values) {
82
+ if (!Array.isArray(values)) return;
83
+ const arr = [];
84
+ for (const v of values) {
85
+ const t = epmTrim(v);
86
+ if (t !== '') arr.push(t);
87
+ }
88
+ if (arr.length) obj[key] = arr;
89
+ }
90
+
91
+ function epmEnumName(value, names) {
92
+ if (typeof value === 'number') return names[value];
93
+ return value; // already a label, or undefined
94
+ }
95
+
96
+ /**
97
+ * RFC 8785 (JCS) canonicalization: recursively sort object keys by UTF-16 code
98
+ * units, then ECMAScript JSON.stringify (minimal escaping, no HTML escaping of
99
+ * & < >, raw non-ASCII, integer numbers). Byte-identical to common/jcs in wasm.
100
+ */
101
+ function epmJcsCanonicalize(value) {
102
+ const sortDeep = (v) => {
103
+ if (Array.isArray(v)) return v.map(sortDeep);
104
+ if (v && typeof v === 'object') {
105
+ const out = {};
106
+ for (const k of Object.keys(v).sort((a, b) => (a < b ? -1 : a > b ? 1 : 0))) {
107
+ out[k] = sortDeep(v[k]);
108
+ }
109
+ return out;
110
+ }
111
+ return v;
112
+ };
113
+ return JSON.stringify(sortDeep(value));
114
+ }
115
+
62
116
  /**
63
- * Build a canonical representation of EPM fields for content signing.
64
- * Excludes SIGNATURE and SIGNATURE_TIMESTAMP (those are the signature itself).
65
- * Includes CHAIN_PROOFS since they are part of the signed content.
117
+ * Build the canonical EPM signing content. Byte-identical to the in-module
118
+ * verifier (common/epm BuildSigningContent + common/jcs Canonicalize), so a
119
+ * wallet signature over this content verifies isomorphically in the browser and
120
+ * on wasmedge. Mirrors the field set/rules exactly: trim + omit-empty strings,
121
+ * enum-label ENTITY_TYPE (always) / KEY_TYPE (Signing|Encryption only), nested
122
+ * ADDRESS, KEYS/CHAIN_PROOFS arrays, SIGNATURE_TIMESTAMP (integer, when nonzero),
123
+ * and SIGNATURE excluded.
66
124
  *
67
- * @param {Object} epm - EPM fields as a plain object
68
- * @returns {Uint8Array} UTF-8 encoded canonical representation
125
+ * @param {Object} epm - EPM fields as a plain object (schema UPPER_SNAKE keys;
126
+ * ENTITY_TYPE/KEY_TYPE may be enum index or label)
127
+ * @returns {Uint8Array} UTF-8 encoded canonical (JCS) representation
69
128
  */
70
129
  export function buildEPMSigningContent(epm) {
71
- // Extract all EPM fields except SIGNATURE and SIGNATURE_TIMESTAMP
72
- const {
73
- SIGNATURE: _sig,
74
- SIGNATURE_TIMESTAMP: _ts,
75
- signature: _sig2,
76
- signature_timestamp: _ts2,
77
- ...contentFields
78
- } = epm;
79
-
80
- // Sort keys for deterministic output
81
- const sorted = Object.keys(contentFields)
82
- .sort()
83
- .reduce((obj, key) => {
84
- obj[key] = contentFields[key];
85
- return obj;
86
- }, {});
87
-
88
- const canonical = JSON.stringify(sorted);
89
- return new TextEncoder().encode(canonical);
130
+ const g = (k) => epm[k] ?? epm[k.toLowerCase()];
131
+ const content = {};
132
+
133
+ epmAddStr(content, 'DN', g('DN'));
134
+ epmAddStr(content, 'LEGAL_NAME', g('LEGAL_NAME'));
135
+ epmAddStr(content, 'FAMILY_NAME', g('FAMILY_NAME'));
136
+ epmAddStr(content, 'GIVEN_NAME', g('GIVEN_NAME'));
137
+ epmAddStr(content, 'ADDITIONAL_NAME', g('ADDITIONAL_NAME'));
138
+ epmAddStr(content, 'HONORIFIC_PREFIX', g('HONORIFIC_PREFIX'));
139
+ epmAddStr(content, 'HONORIFIC_SUFFIX', g('HONORIFIC_SUFFIX'));
140
+ epmAddStr(content, 'JOB_TITLE', g('JOB_TITLE'));
141
+ epmAddStr(content, 'OCCUPATION', g('OCCUPATION'));
142
+ epmAddStr(content, 'EMAIL', g('EMAIL'));
143
+ epmAddStr(content, 'TELEPHONE', g('TELEPHONE'));
144
+
145
+ const addr = g('ADDRESS');
146
+ if (addr && typeof addr === 'object') {
147
+ const a = {};
148
+ const ag = (k) => addr[k] ?? addr[k.toLowerCase()];
149
+ epmAddStr(a, 'COUNTRY', ag('COUNTRY'));
150
+ epmAddStr(a, 'REGION', ag('REGION'));
151
+ epmAddStr(a, 'LOCALITY', ag('LOCALITY'));
152
+ epmAddStr(a, 'POSTAL_CODE', ag('POSTAL_CODE'));
153
+ epmAddStr(a, 'STREET', ag('STREET'));
154
+ epmAddStr(a, 'POST_OFFICE_BOX_NUMBER', ag('POST_OFFICE_BOX_NUMBER'));
155
+ if (Object.keys(a).length) content.ADDRESS = a;
156
+ }
157
+
158
+ epmAddStrArray(content, 'ALTERNATE_NAMES', g('ALTERNATE_NAMES'));
159
+
160
+ const keys = g('KEYS');
161
+ if (Array.isArray(keys)) {
162
+ const arr = [];
163
+ for (const k of keys) {
164
+ if (!k || typeof k !== 'object') continue;
165
+ const e = {};
166
+ const kg = (kk) => k[kk] ?? k[kk.toLowerCase()];
167
+ epmAddStr(e, 'PUBLIC_KEY', kg('PUBLIC_KEY'));
168
+ epmAddStr(e, 'XPUB', kg('XPUB'));
169
+ epmAddStr(e, 'ADDRESS_TYPE', kg('ADDRESS_TYPE'));
170
+ epmAddStr(e, 'KEY_ADDRESS', kg('KEY_ADDRESS'));
171
+ const kt = epmEnumName(kg('KEY_TYPE'), EPM_KEY_TYPE_NAMES);
172
+ if (kt === 'Signing' || kt === 'Encryption') e.KEY_TYPE = kt;
173
+ if (Object.keys(e).length) arr.push(e);
174
+ }
175
+ if (arr.length) content.KEYS = arr;
176
+ }
177
+
178
+ epmAddStrArray(content, 'MULTIFORMAT_ADDRESS', g('MULTIFORMAT_ADDRESS'));
179
+
180
+ // ENTITY_TYPE: always present, verbatim enum label (default User, the FB default).
181
+ const etRaw = g('ENTITY_TYPE');
182
+ const et = etRaw == null ? EPM_ENTITY_TYPE_NAMES[0] : epmEnumName(etRaw, EPM_ENTITY_TYPE_NAMES);
183
+ content.ENTITY_TYPE = typeof et === 'string' ? et : EPM_ENTITY_TYPE_NAMES[0];
184
+
185
+ const ts = g('SIGNATURE_TIMESTAMP');
186
+ const tsNum = Number(ts);
187
+ if (ts != null && Number.isFinite(tsNum) && tsNum !== 0) {
188
+ content.SIGNATURE_TIMESTAMP = Math.trunc(tsNum);
189
+ }
190
+
191
+ const proofs = g('CHAIN_PROOFS');
192
+ if (Array.isArray(proofs)) {
193
+ const arr = [];
194
+ for (const p of proofs) {
195
+ if (!p || typeof p !== 'object') continue;
196
+ const e = {};
197
+ const pg = (kk) => p[kk] ?? p[kk.toLowerCase()];
198
+ epmAddStr(e, 'CHAIN', pg('CHAIN'));
199
+ epmAddStr(e, 'ADDRESS', pg('ADDRESS'));
200
+ epmAddStr(e, 'PUBLIC_KEY', pg('PUBLIC_KEY'));
201
+ epmAddStr(e, 'KEY_PATH', pg('KEY_PATH'));
202
+ epmAddStr(e, 'SIGNATURE', pg('SIGNATURE'));
203
+ epmAddStr(e, 'SIGNED_PAYLOAD', pg('SIGNED_PAYLOAD'));
204
+ epmAddStr(e, 'ALGORITHM', pg('ALGORITHM'));
205
+ epmAddStr(e, 'ENCODING', pg('ENCODING'));
206
+ if (Object.keys(e).length) arr.push(e);
207
+ }
208
+ if (arr.length) content.CHAIN_PROOFS = arr;
209
+ }
210
+
211
+ return new TextEncoder().encode(epmJcsCanonicalize(content));
90
212
  }
91
213
 
92
214
  /**
93
- * Sign EPM content with an Ed25519 private key.
215
+ * Sign EPM content. Default curve is ed25519 (fast, the network default); pass
216
+ * `{ curve: 'secp256k1' }` to sign with secp256k1 (ECDSA-DER over sha256(content),
217
+ * byte-compatible with the Go/C++ EPM verifiers). The content canonicalization is
218
+ * identical for both curves; only the signature differs.
94
219
  *
95
220
  * @param {Object} wallet - Initialized HDWalletModule
96
221
  * @param {Object} epm - EPM fields as a plain object (without SIGNATURE/SIGNATURE_TIMESTAMP)
97
- * @param {Uint8Array} ed25519PrivateKey - 32-byte Ed25519 private key (seed)
222
+ * @param {Uint8Array} privateKey - 32-byte private key (ed25519 seed or secp256k1 key)
223
+ * @param {{ curve?: 'ed25519'|'secp256k1' }} [options]
98
224
  * @returns {{ signature: string, timestamp: number }} Hex signature and Unix timestamp
99
225
  */
100
- export function signEPMContent(wallet, epm, ed25519PrivateKey) {
226
+ export function signEPMContent(wallet, epm, privateKey, options = {}) {
227
+ const curve = String(options.curve || 'ed25519').toLowerCase();
101
228
  const timestamp = Math.floor(Date.now() / 1000);
102
229
  const content = buildEPMSigningContent({ ...epm, SIGNATURE_TIMESTAMP: timestamp });
103
- const sig = wallet.curves.ed25519.sign(content, ed25519PrivateKey);
230
+ const sig =
231
+ curve === 'secp256k1'
232
+ ? wallet.curves.secp256k1.sign(wallet.utils.sha256(content), privateKey)
233
+ : wallet.curves.ed25519.sign(content, privateKey);
104
234
  return {
105
235
  signature: wallet.utils.encodeHex(sig),
106
236
  timestamp,
@@ -108,20 +238,30 @@ export function signEPMContent(wallet, epm, ed25519PrivateKey) {
108
238
  }
109
239
 
110
240
  /**
111
- * Verify an EPM content signature.
241
+ * Verify an EPM content signature. Dispatches on the explicit `options.curve`,
242
+ * else infers from the public key length (32 = ed25519; 33/65 = secp256k1).
243
+ * secp256k1 is verified as ECDSA-DER over sha256(content), matching signEPMContent
244
+ * and the Go/C++ verifiers.
112
245
  *
113
246
  * @param {Object} wallet - Initialized HDWalletModule
114
247
  * @param {Object} epm - Full EPM object including SIGNATURE and SIGNATURE_TIMESTAMP
115
- * @param {Uint8Array} ed25519PublicKey - 32-byte Ed25519 public key
248
+ * @param {Uint8Array} publicKey - ed25519 (32B) or secp256k1 (33/65B) public key
249
+ * @param {{ curve?: 'ed25519'|'secp256k1' }} [options]
116
250
  * @returns {boolean} True if signature is valid
117
251
  */
118
- export function verifyEPMSignature(wallet, epm, ed25519PublicKey) {
252
+ export function verifyEPMSignature(wallet, epm, publicKey, options = {}) {
119
253
  const sigHex = epm.SIGNATURE || epm.signature;
120
254
  if (!sigHex) return false;
121
255
 
122
256
  const content = buildEPMSigningContent(epm);
123
257
  const sig = wallet.utils.decodeHex(sigHex);
124
- return wallet.curves.ed25519.verify(content, sig, ed25519PublicKey);
258
+ const curve = String(
259
+ options.curve || (publicKey && publicKey.length === 32 ? 'ed25519' : 'secp256k1'),
260
+ ).toLowerCase();
261
+ if (curve === 'secp256k1') {
262
+ return wallet.curves.secp256k1.verify(wallet.utils.sha256(content), sig, publicKey);
263
+ }
264
+ return wallet.curves.ed25519.verify(content, sig, publicKey);
125
265
  }
126
266
 
127
267
  // =============================================================================
package/src/index.mjs CHANGED
@@ -9,7 +9,7 @@
9
9
  * - Transaction building and signing
10
10
  *
11
11
  * @module hd-wallet-wasm
12
- * @version 2.0.19
12
+ * @version 2.0.9
13
13
  */
14
14
 
15
15
  // Import aligned API for batch operations