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 +1 -1
- package/src/epm-attestation.mjs +172 -32
- package/src/index.mjs +1 -1
package/package.json
CHANGED
package/src/epm-attestation.mjs
CHANGED
|
@@ -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
|
|
64
|
-
*
|
|
65
|
-
*
|
|
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
|
-
*
|
|
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
|
-
|
|
72
|
-
const {
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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
|
|
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}
|
|
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,
|
|
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 =
|
|
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}
|
|
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,
|
|
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
|
-
|
|
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
|
// =============================================================================
|