@oxy.so/protocol 1.2.1 → 1.3.0
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/cjs/.tsbuildinfo +1 -1
- package/dist/cjs/chain/engine.js +1 -1
- package/dist/cjs/chain/recordStore.js +3 -3
- package/dist/cjs/chain/verify.js +8 -4
- package/dist/cjs/envelope/signingInput.js +12 -1
- package/dist/cjs/identity/resolver.js +2 -1
- package/dist/cjs/node/didWebResolver.js +1 -3
- package/dist/cjs/node/nodeApp.js +9 -3
- package/dist/cjs/node/nodeClient.js +7 -2
- package/dist/cjs/platform/platform.js +1 -1
- package/dist/cjs/secp256k1.js +25 -26
- package/dist/cjs/sign.js +17 -0
- package/dist/esm/.tsbuildinfo +1 -1
- package/dist/esm/chain/engine.js +1 -1
- package/dist/esm/chain/recordStore.js +3 -3
- package/dist/esm/chain/verify.js +8 -4
- package/dist/esm/envelope/sign.js +1 -1
- package/dist/esm/envelope/signingInput.js +12 -1
- package/dist/esm/identity/resolver.js +2 -1
- package/dist/esm/node/didWebResolver.js +1 -3
- package/dist/esm/node/nodeApp.js +9 -3
- package/dist/esm/node/nodeClient.js +8 -3
- package/dist/esm/platform/platform.js +1 -1
- package/dist/esm/secp256k1.js +26 -27
- package/dist/esm/sign.js +10 -0
- package/dist/types/.tsbuildinfo +1 -1
- package/dist/types/chain/engine.d.ts +1 -1
- package/dist/types/chain/recordStore.d.ts +3 -3
- package/dist/types/chain/verify.d.ts +3 -2
- package/dist/types/sign.d.ts +10 -0
- package/package.json +22 -8
- package/src/__tests__/canonicalJson.test.ts +2 -6
- package/src/__tests__/chain.test.ts +42 -11
- package/src/__tests__/commonsIdentityBridge.test.ts +7 -2
- package/src/__tests__/didWebResolver.test.ts +36 -7
- package/src/__tests__/nodeApp.test.ts +76 -21
- package/src/__tests__/nodeClient.test.ts +30 -7
- package/src/__tests__/nodeHarness.ts +7 -3
- package/src/__tests__/optionalNativePeers.test.ts +8 -6
- package/src/__tests__/rateLimit.test.ts +2 -1
- package/src/__tests__/registrationPow.test.ts +35 -37
- package/src/__tests__/secp256k1.test.ts +41 -70
- package/src/__tests__/transparency.test.ts +4 -1
- package/src/auth/registrationPow.ts +5 -1
- package/src/chain/engine.ts +1 -1
- package/src/chain/recordStore.ts +3 -3
- package/src/chain/verify.ts +11 -4
- package/src/envelope/recordId.ts +1 -4
- package/src/envelope/sign.ts +6 -6
- package/src/envelope/signingInput.ts +12 -1
- package/src/identity/resolver.ts +9 -2
- package/src/node/didWebResolver.ts +2 -7
- package/src/node/nodeApp.ts +85 -67
- package/src/node/nodeClient.ts +35 -16
- package/src/platform/crypto.native.ts +9 -2
- package/src/platform/platform.ts +3 -1
- package/src/secp256k1.ts +33 -58
- package/src/sign.ts +16 -0
package/src/node/nodeApp.ts
CHANGED
|
@@ -261,7 +261,10 @@ export function createNodeApp(deps: NodeAppDependencies): NodeApp {
|
|
|
261
261
|
mode: config.mode,
|
|
262
262
|
version: config.protocolId,
|
|
263
263
|
serviceType: config.serviceType,
|
|
264
|
-
head:
|
|
264
|
+
head:
|
|
265
|
+
head && head.headRecordId !== null
|
|
266
|
+
? { seq: head.seq, headRecordId: head.headRecordId }
|
|
267
|
+
: null,
|
|
265
268
|
});
|
|
266
269
|
} catch (error) {
|
|
267
270
|
next(error);
|
|
@@ -288,7 +291,10 @@ export function createNodeApp(deps: NodeAppDependencies): NodeApp {
|
|
|
288
291
|
const since = firstQueryValue(req.query.since);
|
|
289
292
|
const limit = parseLimit(req.query.limit);
|
|
290
293
|
const head = await store.getHead(subject);
|
|
291
|
-
const headWire =
|
|
294
|
+
const headWire =
|
|
295
|
+
head && head.headRecordId !== null
|
|
296
|
+
? { seq: head.seq, headRecordId: head.headRecordId }
|
|
297
|
+
: null;
|
|
292
298
|
|
|
293
299
|
const sinceSeq = await resolveSinceSeq(store, subject, since);
|
|
294
300
|
if (sinceSeq === null) {
|
|
@@ -305,80 +311,92 @@ export function createNodeApp(deps: NodeAppDependencies): NodeApp {
|
|
|
305
311
|
});
|
|
306
312
|
|
|
307
313
|
// ── Owner write: a single signed envelope ────────────────────────────────────
|
|
308
|
-
app.post(
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
if (!ownerAuth.isOwnerKey(verification.envelope.publicKey)) {
|
|
316
|
-
res.status(403).json({ error: 'not_owner' });
|
|
317
|
-
return;
|
|
318
|
-
}
|
|
319
|
-
if (!isCollectionAllowed(config, verification.envelope.collection ?? '')) {
|
|
320
|
-
res.status(403).json({ error: 'foreign_collection' });
|
|
321
|
-
return;
|
|
322
|
-
}
|
|
323
|
-
const outcome = await store.append(subject, verification.envelope, verification.recordId);
|
|
324
|
-
if (!outcome.ok) {
|
|
325
|
-
res.status(appendStatus(outcome.reason)).json({ error: outcome.reason });
|
|
326
|
-
return;
|
|
327
|
-
}
|
|
328
|
-
res.status(201).json({ recordId: outcome.recordId, seq: outcome.seq });
|
|
329
|
-
} catch (error) {
|
|
330
|
-
next(error);
|
|
331
|
-
}
|
|
332
|
-
});
|
|
333
|
-
|
|
334
|
-
// ── Owner write: a batch push (verified + appended in order) ──────────────────
|
|
335
|
-
app.post(NODE_SYNC_PUSH_PATH, writeRateLimit, jsonParser, async (req: Request, res: Response, next: NextFunction) => {
|
|
336
|
-
try {
|
|
337
|
-
const body: unknown = req.body;
|
|
338
|
-
const items =
|
|
339
|
-
typeof body === 'object' && body !== null && Array.isArray((body as { records?: unknown }).records)
|
|
340
|
-
? (body as { records: unknown[] }).records
|
|
341
|
-
: null;
|
|
342
|
-
if (!items) {
|
|
343
|
-
res.status(400).json({ error: 'invalid_batch' });
|
|
344
|
-
return;
|
|
345
|
-
}
|
|
346
|
-
if (items.length > MAX_SYNC_BATCH) {
|
|
347
|
-
res.status(400).json({ error: 'batch_too_large' });
|
|
348
|
-
return;
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
const results: Array<
|
|
352
|
-
{ ok: true; recordId: string; seq: number } | { ok: false; reason: string }
|
|
353
|
-
> = [];
|
|
354
|
-
for (const item of items) {
|
|
355
|
-
const verification = await verifyNodeRecordEnvelope(item);
|
|
314
|
+
app.post(
|
|
315
|
+
NODE_RECORDS_PATH,
|
|
316
|
+
writeRateLimit,
|
|
317
|
+
jsonParser,
|
|
318
|
+
async (req: Request, res: Response, next: NextFunction) => {
|
|
319
|
+
try {
|
|
320
|
+
const verification = await verifyNodeRecordEnvelope(req.body);
|
|
356
321
|
if (!verification.ok) {
|
|
357
|
-
|
|
358
|
-
|
|
322
|
+
res.status(400).json({ error: verification.reason });
|
|
323
|
+
return;
|
|
359
324
|
}
|
|
360
325
|
if (!ownerAuth.isOwnerKey(verification.envelope.publicKey)) {
|
|
361
|
-
|
|
362
|
-
|
|
326
|
+
res.status(403).json({ error: 'not_owner' });
|
|
327
|
+
return;
|
|
363
328
|
}
|
|
364
329
|
if (!isCollectionAllowed(config, verification.envelope.collection ?? '')) {
|
|
365
|
-
|
|
366
|
-
|
|
330
|
+
res.status(403).json({ error: 'foreign_collection' });
|
|
331
|
+
return;
|
|
367
332
|
}
|
|
368
333
|
const outcome = await store.append(subject, verification.envelope, verification.recordId);
|
|
369
|
-
|
|
370
|
-
outcome.
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
);
|
|
334
|
+
if (!outcome.ok) {
|
|
335
|
+
res.status(appendStatus(outcome.reason)).json({ error: outcome.reason });
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
res.status(201).json({ recordId: outcome.recordId, seq: outcome.seq });
|
|
339
|
+
} catch (error) {
|
|
340
|
+
next(error);
|
|
374
341
|
}
|
|
342
|
+
},
|
|
343
|
+
);
|
|
375
344
|
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
345
|
+
// ── Owner write: a batch push (verified + appended in order) ──────────────────
|
|
346
|
+
app.post(
|
|
347
|
+
NODE_SYNC_PUSH_PATH,
|
|
348
|
+
writeRateLimit,
|
|
349
|
+
jsonParser,
|
|
350
|
+
async (req: Request, res: Response, next: NextFunction) => {
|
|
351
|
+
try {
|
|
352
|
+
const body: unknown = req.body;
|
|
353
|
+
const items =
|
|
354
|
+
typeof body === 'object' &&
|
|
355
|
+
body !== null &&
|
|
356
|
+
Array.isArray((body as { records?: unknown }).records)
|
|
357
|
+
? (body as { records: unknown[] }).records
|
|
358
|
+
: null;
|
|
359
|
+
if (!items) {
|
|
360
|
+
res.status(400).json({ error: 'invalid_batch' });
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
if (items.length > MAX_SYNC_BATCH) {
|
|
364
|
+
res.status(400).json({ error: 'batch_too_large' });
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
const results: Array<
|
|
369
|
+
{ ok: true; recordId: string; seq: number } | { ok: false; reason: string }
|
|
370
|
+
> = [];
|
|
371
|
+
for (const item of items) {
|
|
372
|
+
const verification = await verifyNodeRecordEnvelope(item);
|
|
373
|
+
if (!verification.ok) {
|
|
374
|
+
results.push({ ok: false, reason: verification.reason });
|
|
375
|
+
continue;
|
|
376
|
+
}
|
|
377
|
+
if (!ownerAuth.isOwnerKey(verification.envelope.publicKey)) {
|
|
378
|
+
results.push({ ok: false, reason: 'not_owner' });
|
|
379
|
+
continue;
|
|
380
|
+
}
|
|
381
|
+
if (!isCollectionAllowed(config, verification.envelope.collection ?? '')) {
|
|
382
|
+
results.push({ ok: false, reason: 'foreign_collection' });
|
|
383
|
+
continue;
|
|
384
|
+
}
|
|
385
|
+
const outcome = await store.append(subject, verification.envelope, verification.recordId);
|
|
386
|
+
results.push(
|
|
387
|
+
outcome.ok
|
|
388
|
+
? { ok: true, recordId: outcome.recordId, seq: outcome.seq }
|
|
389
|
+
: { ok: false, reason: outcome.reason },
|
|
390
|
+
);
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
const accepted = results.filter((result) => result.ok).length;
|
|
394
|
+
res.json({ accepted, results });
|
|
395
|
+
} catch (error) {
|
|
396
|
+
next(error);
|
|
397
|
+
}
|
|
398
|
+
},
|
|
399
|
+
);
|
|
382
400
|
|
|
383
401
|
// ── Serve a content-addressed blob ───────────────────────────────────────────
|
|
384
402
|
app.get(`${NODE_BLOBS_PATH}/:hash`, async (req: Request, res: Response, next: NextFunction) => {
|
package/src/node/nodeClient.ts
CHANGED
|
@@ -13,12 +13,7 @@
|
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
15
|
import type { SignedRecordEnvelope } from '@oxy.so/contracts';
|
|
16
|
-
import {
|
|
17
|
-
type NodeFetch,
|
|
18
|
-
type NodeFetchInit,
|
|
19
|
-
readBoundedBytes,
|
|
20
|
-
readBoundedJson,
|
|
21
|
-
} from './httpFetch';
|
|
16
|
+
import { type NodeFetch, type NodeFetchInit, readBoundedBytes, readBoundedJson } from './httpFetch';
|
|
22
17
|
import {
|
|
23
18
|
DEFAULT_CLIENT_MAX_REDIRECTS,
|
|
24
19
|
DEFAULT_CLIENT_TIMEOUT_MS,
|
|
@@ -105,7 +100,11 @@ export interface NodeClientOptions {
|
|
|
105
100
|
}
|
|
106
101
|
|
|
107
102
|
function readError(body: unknown): string | undefined {
|
|
108
|
-
if (
|
|
103
|
+
if (
|
|
104
|
+
typeof body === 'object' &&
|
|
105
|
+
body !== null &&
|
|
106
|
+
typeof (body as { error?: unknown }).error === 'string'
|
|
107
|
+
) {
|
|
109
108
|
return (body as { error: string }).error;
|
|
110
109
|
}
|
|
111
110
|
return undefined;
|
|
@@ -200,7 +199,10 @@ export class NodeClient {
|
|
|
200
199
|
headRaw !== null &&
|
|
201
200
|
typeof (headRaw as { seq?: unknown }).seq === 'number' &&
|
|
202
201
|
typeof (headRaw as { headRecordId?: unknown }).headRecordId === 'string'
|
|
203
|
-
? {
|
|
202
|
+
? {
|
|
203
|
+
seq: (headRaw as { seq: number }).seq,
|
|
204
|
+
headRecordId: (headRaw as { headRecordId: string }).headRecordId,
|
|
205
|
+
}
|
|
204
206
|
: null;
|
|
205
207
|
return { records, count: records.length, head };
|
|
206
208
|
}
|
|
@@ -231,7 +233,10 @@ export class NodeClient {
|
|
|
231
233
|
}
|
|
232
234
|
const obj = body as { recordId?: unknown; seq?: unknown };
|
|
233
235
|
if (typeof obj.recordId !== 'string' || typeof obj.seq !== 'number') {
|
|
234
|
-
throw new NodeClientError(
|
|
236
|
+
throw new NodeClientError(
|
|
237
|
+
`node ${NODE_RECORDS_PATH} returned a malformed write result`,
|
|
238
|
+
res.status,
|
|
239
|
+
);
|
|
235
240
|
}
|
|
236
241
|
return { recordId: obj.recordId, seq: obj.seq };
|
|
237
242
|
}
|
|
@@ -241,9 +246,10 @@ export class NodeClient {
|
|
|
241
246
|
* node's per-item results. Throws {@link NodeClientError} only on a non-2xx
|
|
242
247
|
* batch-level failure (`invalid_batch` / `batch_too_large`).
|
|
243
248
|
*/
|
|
244
|
-
async pushRecords(
|
|
245
|
-
|
|
246
|
-
|
|
249
|
+
async pushRecords(envelopes: SignedRecordEnvelope[]): Promise<{
|
|
250
|
+
accepted: number;
|
|
251
|
+
results: Array<{ ok: boolean; recordId?: string; seq?: number; reason?: string }>;
|
|
252
|
+
}> {
|
|
247
253
|
const res = await this.fetch(
|
|
248
254
|
`${this.baseUrl}${NODE_SYNC_PUSH_PATH}`,
|
|
249
255
|
this.init({
|
|
@@ -272,14 +278,20 @@ export class NodeClient {
|
|
|
272
278
|
|
|
273
279
|
/** Fetch a content-addressed blob. Returns `null` on a 404; throws on other non-2xx. */
|
|
274
280
|
async getBlob(hash: string): Promise<Buffer | null> {
|
|
275
|
-
const res = await this.fetch(
|
|
281
|
+
const res = await this.fetch(
|
|
282
|
+
`${this.baseUrl}${NODE_BLOBS_PATH}/${encodeURIComponent(hash)}`,
|
|
283
|
+
this.init({ method: 'GET' }),
|
|
284
|
+
);
|
|
276
285
|
if (res.status === 404) {
|
|
277
286
|
res.destroy();
|
|
278
287
|
return null;
|
|
279
288
|
}
|
|
280
289
|
if (res.status < 200 || res.status >= 300) {
|
|
281
290
|
res.destroy();
|
|
282
|
-
throw new NodeClientError(
|
|
291
|
+
throw new NodeClientError(
|
|
292
|
+
`node ${NODE_BLOBS_PATH}/:hash responded HTTP ${res.status}`,
|
|
293
|
+
res.status,
|
|
294
|
+
);
|
|
283
295
|
}
|
|
284
296
|
return readBoundedBytes(res, this.blobMaxBytes);
|
|
285
297
|
}
|
|
@@ -290,7 +302,11 @@ export class NodeClient {
|
|
|
290
302
|
* passes the resulting `{ publicKey, signature, timestamp }`; the client sets
|
|
291
303
|
* the owner-auth headers. Throws {@link NodeClientError} on a non-2xx.
|
|
292
304
|
*/
|
|
293
|
-
async putBlob(
|
|
305
|
+
async putBlob(
|
|
306
|
+
hash: string,
|
|
307
|
+
bytes: Uint8Array,
|
|
308
|
+
auth: NodeBlobPinAuth,
|
|
309
|
+
): Promise<NodeBlobPutResult> {
|
|
294
310
|
const res = await this.fetch(
|
|
295
311
|
`${this.baseUrl}${NODE_BLOBS_PATH}/${encodeURIComponent(hash)}`,
|
|
296
312
|
this.init({
|
|
@@ -315,7 +331,10 @@ export class NodeClient {
|
|
|
315
331
|
}
|
|
316
332
|
const obj = body as { hash?: unknown; size?: unknown };
|
|
317
333
|
if (typeof obj.hash !== 'string' || typeof obj.size !== 'number') {
|
|
318
|
-
throw new NodeClientError(
|
|
334
|
+
throw new NodeClientError(
|
|
335
|
+
`node ${NODE_BLOBS_PATH}/:hash returned a malformed pin result`,
|
|
336
|
+
res.status,
|
|
337
|
+
);
|
|
319
338
|
}
|
|
320
339
|
return { hash: obj.hash, size: obj.size };
|
|
321
340
|
}
|
|
@@ -244,7 +244,10 @@ function narrowSocialReceive(value: unknown): CommonsSocialReceiveSignature | nu
|
|
|
244
244
|
}
|
|
245
245
|
|
|
246
246
|
/** Run one native call; a rejection or an unexpected shape is `null`. */
|
|
247
|
-
async function ask<T>(
|
|
247
|
+
async function ask<T>(
|
|
248
|
+
call: () => Promise<unknown>,
|
|
249
|
+
narrow: (value: unknown) => T | null,
|
|
250
|
+
): Promise<T | null> {
|
|
248
251
|
try {
|
|
249
252
|
return narrow(await call());
|
|
250
253
|
} catch {
|
|
@@ -271,7 +274,11 @@ export function loadCommonsIdentityBridge(): Promise<CommonsIdentityBridge | nul
|
|
|
271
274
|
return {
|
|
272
275
|
describe: () => ask(() => m.describe(), narrowDescription),
|
|
273
276
|
proveIdentity: (challenge: string) => ask(() => m.proveIdentity(challenge), narrowProof),
|
|
274
|
-
deriveScopedSeed: (info: string) =>
|
|
277
|
+
deriveScopedSeed: (info: string) =>
|
|
278
|
+
ask(
|
|
279
|
+
() => m.deriveScopedSeed(info),
|
|
280
|
+
(v) => hexField(v, 64),
|
|
281
|
+
),
|
|
275
282
|
signSocialReceive: (index: number, digest: string) =>
|
|
276
283
|
ask(() => m.signSocialReceive(index, digest), narrowSocialReceive),
|
|
277
284
|
} satisfies CommonsIdentityBridge;
|
package/src/platform/platform.ts
CHANGED
|
@@ -27,5 +27,7 @@ export function isReactNative(): boolean {
|
|
|
27
27
|
* `randomBytes`) and the `await import('node:crypto')` loader.
|
|
28
28
|
*/
|
|
29
29
|
export function isNodeJS(): boolean {
|
|
30
|
-
return
|
|
30
|
+
return (
|
|
31
|
+
typeof process !== 'undefined' && process.versions != null && process.versions.node != null
|
|
32
|
+
);
|
|
31
33
|
}
|
package/src/secp256k1.ts
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* ECDH x-coordinate. Callers never receive a library-specific key object.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import { secp256k1 } from
|
|
10
|
+
import { secp256k1 } from '@noble/curves/secp256k1.js';
|
|
11
11
|
|
|
12
12
|
const HEX = /^[0-9a-fA-F]+$/;
|
|
13
13
|
const COMPRESSED_PUBLIC_KEY = /^(02|03)[0-9a-fA-F]{64}$/;
|
|
@@ -26,9 +26,9 @@ export interface Secp256k1SignatureOptions {
|
|
|
26
26
|
}
|
|
27
27
|
|
|
28
28
|
function bytesToHex(bytes: Uint8Array): string {
|
|
29
|
-
let result =
|
|
29
|
+
let result = '';
|
|
30
30
|
for (const byte of bytes) {
|
|
31
|
-
result += byte.toString(16).padStart(2,
|
|
31
|
+
result += byte.toString(16).padStart(2, '0');
|
|
32
32
|
}
|
|
33
33
|
return result;
|
|
34
34
|
}
|
|
@@ -51,20 +51,18 @@ function hexToBytes(value: string, label: string): Uint8Array {
|
|
|
51
51
|
*/
|
|
52
52
|
export function normalizeSecp256k1PrivateKey(privateKeyHex: string): string {
|
|
53
53
|
if (
|
|
54
|
-
typeof privateKeyHex !==
|
|
54
|
+
typeof privateKeyHex !== 'string' ||
|
|
55
55
|
privateKeyHex.length === 0 ||
|
|
56
56
|
privateKeyHex.length > 64 ||
|
|
57
57
|
!HEX.test(privateKeyHex)
|
|
58
58
|
) {
|
|
59
|
-
throw new Error(
|
|
60
|
-
"secp256k1 private key must contain 1 to 64 hexadecimal characters",
|
|
61
|
-
);
|
|
59
|
+
throw new Error('secp256k1 private key must contain 1 to 64 hexadecimal characters');
|
|
62
60
|
}
|
|
63
61
|
|
|
64
|
-
const normalized = privateKeyHex.toLowerCase().padStart(64,
|
|
65
|
-
const privateKey = hexToBytes(normalized,
|
|
62
|
+
const normalized = privateKeyHex.toLowerCase().padStart(64, '0');
|
|
63
|
+
const privateKey = hexToBytes(normalized, 'secp256k1 private key');
|
|
66
64
|
if (!secp256k1.utils.isValidSecretKey(privateKey)) {
|
|
67
|
-
throw new Error(
|
|
65
|
+
throw new Error('secp256k1 private key is outside the valid scalar range');
|
|
68
66
|
}
|
|
69
67
|
return normalized;
|
|
70
68
|
}
|
|
@@ -80,36 +78,28 @@ export function isValidSecp256k1PrivateKey(privateKeyHex: string): boolean {
|
|
|
80
78
|
}
|
|
81
79
|
|
|
82
80
|
function parsePrivateKey(privateKeyHex: string): Uint8Array {
|
|
83
|
-
return hexToBytes(
|
|
84
|
-
normalizeSecp256k1PrivateKey(privateKeyHex),
|
|
85
|
-
"secp256k1 private key",
|
|
86
|
-
);
|
|
81
|
+
return hexToBytes(normalizeSecp256k1PrivateKey(privateKeyHex), 'secp256k1 private key');
|
|
87
82
|
}
|
|
88
83
|
|
|
89
84
|
function parsePublicKey(publicKeyHex: string): Uint8Array {
|
|
90
85
|
if (
|
|
91
|
-
typeof publicKeyHex !==
|
|
92
|
-
(!COMPRESSED_PUBLIC_KEY.test(publicKeyHex) &&
|
|
93
|
-
!UNCOMPRESSED_PUBLIC_KEY.test(publicKeyHex))
|
|
86
|
+
typeof publicKeyHex !== 'string' ||
|
|
87
|
+
(!COMPRESSED_PUBLIC_KEY.test(publicKeyHex) && !UNCOMPRESSED_PUBLIC_KEY.test(publicKeyHex))
|
|
94
88
|
) {
|
|
95
|
-
throw new Error(
|
|
96
|
-
"secp256k1 public key must be a compressed or uncompressed SEC1 hex key",
|
|
97
|
-
);
|
|
89
|
+
throw new Error('secp256k1 public key must be a compressed or uncompressed SEC1 hex key');
|
|
98
90
|
}
|
|
99
91
|
|
|
100
|
-
const publicKey = hexToBytes(publicKeyHex,
|
|
92
|
+
const publicKey = hexToBytes(publicKeyHex, 'secp256k1 public key');
|
|
101
93
|
// Parsing validates the SEC1 prefix, coordinate range, and curve equation.
|
|
102
94
|
secp256k1.Point.fromBytes(publicKey);
|
|
103
95
|
return publicKey;
|
|
104
96
|
}
|
|
105
97
|
|
|
106
98
|
function parseDigest(digestHex: string): Uint8Array {
|
|
107
|
-
if (typeof digestHex !==
|
|
108
|
-
throw new Error(
|
|
109
|
-
"secp256k1 digest must be exactly 32 bytes of hexadecimal data",
|
|
110
|
-
);
|
|
99
|
+
if (typeof digestHex !== 'string' || digestHex.length !== 64) {
|
|
100
|
+
throw new Error('secp256k1 digest must be exactly 32 bytes of hexadecimal data');
|
|
111
101
|
}
|
|
112
|
-
return hexToBytes(digestHex,
|
|
102
|
+
return hexToBytes(digestHex, 'secp256k1 digest');
|
|
113
103
|
}
|
|
114
104
|
|
|
115
105
|
/** True for a valid compressed or uncompressed SEC1 secp256k1 public key. */
|
|
@@ -132,20 +122,12 @@ export function generateSecp256k1KeyPair(): Secp256k1KeyPair {
|
|
|
132
122
|
}
|
|
133
123
|
|
|
134
124
|
/** Derive a compressed or uncompressed SEC1 public key from a private scalar. */
|
|
135
|
-
export function deriveSecp256k1PublicKey(
|
|
136
|
-
privateKeyHex
|
|
137
|
-
compressed = false,
|
|
138
|
-
): string {
|
|
139
|
-
return bytesToHex(
|
|
140
|
-
secp256k1.getPublicKey(parsePrivateKey(privateKeyHex), compressed),
|
|
141
|
-
);
|
|
125
|
+
export function deriveSecp256k1PublicKey(privateKeyHex: string, compressed = false): string {
|
|
126
|
+
return bytesToHex(secp256k1.getPublicKey(parsePrivateKey(privateKeyHex), compressed));
|
|
142
127
|
}
|
|
143
128
|
|
|
144
129
|
/** Parse and re-encode a public key in canonical lowercase SEC1 form. */
|
|
145
|
-
export function normalizeSecp256k1PublicKey(
|
|
146
|
-
publicKeyHex: string,
|
|
147
|
-
compressed = false,
|
|
148
|
-
): string {
|
|
130
|
+
export function normalizeSecp256k1PublicKey(publicKeyHex: string, compressed = false): string {
|
|
149
131
|
const point = secp256k1.Point.fromBytes(parsePublicKey(publicKeyHex));
|
|
150
132
|
return point.toHex(compressed);
|
|
151
133
|
}
|
|
@@ -162,14 +144,10 @@ export function signSecp256k1Digest(
|
|
|
162
144
|
digestHex: string,
|
|
163
145
|
options: Secp256k1SignatureOptions = {},
|
|
164
146
|
): string {
|
|
165
|
-
const signature = secp256k1.sign(
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
lowS: options.lowS ?? false,
|
|
170
|
-
},
|
|
171
|
-
);
|
|
172
|
-
return signature.toHex("der");
|
|
147
|
+
const signature = secp256k1.sign(parseDigest(digestHex), parsePrivateKey(privateKeyHex), {
|
|
148
|
+
lowS: options.lowS ?? false,
|
|
149
|
+
});
|
|
150
|
+
return signature.toHex('der');
|
|
173
151
|
}
|
|
174
152
|
|
|
175
153
|
/** Verify a DER-encoded ECDSA signature, accepting historical high-S signatures. */
|
|
@@ -180,12 +158,12 @@ export function verifySecp256k1Digest(
|
|
|
180
158
|
): boolean {
|
|
181
159
|
const publicKey = parsePublicKey(publicKeyHex);
|
|
182
160
|
const digest = parseDigest(digestHex);
|
|
183
|
-
const signature = hexToBytes(signatureDerHex,
|
|
161
|
+
const signature = hexToBytes(signatureDerHex, 'secp256k1 DER signature');
|
|
184
162
|
|
|
185
163
|
// Parse once up front so malformed or non-DER input is rejected explicitly.
|
|
186
|
-
secp256k1.Signature.fromBytes(signature,
|
|
164
|
+
secp256k1.Signature.fromBytes(signature, 'der');
|
|
187
165
|
return secp256k1.verify(signature, digest, publicKey, {
|
|
188
|
-
format:
|
|
166
|
+
format: 'der',
|
|
189
167
|
lowS: false,
|
|
190
168
|
});
|
|
191
169
|
}
|
|
@@ -197,24 +175,21 @@ export function verifySecp256k1Digest(
|
|
|
197
175
|
* Throws, as BIP32 requires, when the tweak is not below the curve order or
|
|
198
176
|
* the sum is zero: that child is invalid and the caller must not use it.
|
|
199
177
|
*/
|
|
200
|
-
export function tweakAddSecp256k1PrivateKey(
|
|
201
|
-
privateKeyHex: string,
|
|
202
|
-
tweakHex: string,
|
|
203
|
-
): string {
|
|
178
|
+
export function tweakAddSecp256k1PrivateKey(privateKeyHex: string, tweakHex: string): string {
|
|
204
179
|
const n = secp256k1.Point.Fn.ORDER;
|
|
205
180
|
const key = BigInt(`0x${normalizeSecp256k1PrivateKey(privateKeyHex)}`);
|
|
206
|
-
if (typeof tweakHex !==
|
|
207
|
-
throw new Error(
|
|
181
|
+
if (typeof tweakHex !== 'string' || tweakHex.length !== 64 || !HEX.test(tweakHex)) {
|
|
182
|
+
throw new Error('secp256k1 tweak must be exactly 32 bytes of hexadecimal data');
|
|
208
183
|
}
|
|
209
184
|
const tweak = BigInt(`0x${tweakHex}`);
|
|
210
185
|
if (tweak >= n) {
|
|
211
|
-
throw new Error(
|
|
186
|
+
throw new Error('secp256k1 tweak is not below the curve order');
|
|
212
187
|
}
|
|
213
188
|
const child = (key + tweak) % n;
|
|
214
189
|
if (child === 0n) {
|
|
215
|
-
throw new Error(
|
|
190
|
+
throw new Error('secp256k1 tweaked private key is zero');
|
|
216
191
|
}
|
|
217
|
-
return child.toString(16).padStart(64,
|
|
192
|
+
return child.toString(16).padStart(64, '0');
|
|
218
193
|
}
|
|
219
194
|
|
|
220
195
|
/** Derive the fixed-width 32-byte ECDH x-coordinate shared secret. */
|
|
@@ -228,7 +203,7 @@ export function deriveSecp256k1SharedSecret(
|
|
|
228
203
|
true,
|
|
229
204
|
);
|
|
230
205
|
if (encodedPoint.length !== 33) {
|
|
231
|
-
throw new Error(
|
|
206
|
+
throw new Error('secp256k1 ECDH returned an unexpected point encoding');
|
|
232
207
|
}
|
|
233
208
|
return encodedPoint.slice(1);
|
|
234
209
|
}
|
package/src/sign.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@oxy.so/protocol/sign` — message and envelope signatures (secp256k1).
|
|
3
|
+
*
|
|
4
|
+
* The same functions the root entry exports, on their own entry so a client
|
|
5
|
+
* can load the signer (and `@noble/curves` under it) where it signs — inside
|
|
6
|
+
* `@oxy.so/core/crypto`, which apps load on first use — while the root entry
|
|
7
|
+
* it imports at startup for hashing and platform loaders stays free of the
|
|
8
|
+
* curve library once a bundler drops the root's unused re-exports.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
export {
|
|
12
|
+
signEnvelope,
|
|
13
|
+
signMessage,
|
|
14
|
+
verifyEnvelopeSignature,
|
|
15
|
+
verifySignature,
|
|
16
|
+
} from './envelope/sign';
|