@oxy.so/protocol 1.2.2 → 1.3.1
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/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/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/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 +21 -22
- 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/dist/cjs/.tsbuildinfo +0 -1
- package/dist/esm/.tsbuildinfo +0 -1
- package/dist/types/.tsbuildinfo +0 -1
package/dist/esm/secp256k1.js
CHANGED
|
@@ -6,14 +6,14 @@
|
|
|
6
6
|
* RFC 6979 deterministic ECDSA signatures encoded as DER, and the 32-byte
|
|
7
7
|
* ECDH x-coordinate. Callers never receive a library-specific key object.
|
|
8
8
|
*/
|
|
9
|
-
import { secp256k1 } from
|
|
9
|
+
import { secp256k1 } from '@noble/curves/secp256k1.js';
|
|
10
10
|
const HEX = /^[0-9a-fA-F]+$/;
|
|
11
11
|
const COMPRESSED_PUBLIC_KEY = /^(02|03)[0-9a-fA-F]{64}$/;
|
|
12
12
|
const UNCOMPRESSED_PUBLIC_KEY = /^04[0-9a-fA-F]{128}$/;
|
|
13
13
|
function bytesToHex(bytes) {
|
|
14
|
-
let result =
|
|
14
|
+
let result = '';
|
|
15
15
|
for (const byte of bytes) {
|
|
16
|
-
result += byte.toString(16).padStart(2,
|
|
16
|
+
result += byte.toString(16).padStart(2, '0');
|
|
17
17
|
}
|
|
18
18
|
return result;
|
|
19
19
|
}
|
|
@@ -32,16 +32,16 @@ function hexToBytes(value, label) {
|
|
|
32
32
|
* The scalar must be within the secp256k1 order; zero and overflow are rejected.
|
|
33
33
|
*/
|
|
34
34
|
export function normalizeSecp256k1PrivateKey(privateKeyHex) {
|
|
35
|
-
if (typeof privateKeyHex !==
|
|
35
|
+
if (typeof privateKeyHex !== 'string' ||
|
|
36
36
|
privateKeyHex.length === 0 ||
|
|
37
37
|
privateKeyHex.length > 64 ||
|
|
38
38
|
!HEX.test(privateKeyHex)) {
|
|
39
|
-
throw new Error(
|
|
39
|
+
throw new Error('secp256k1 private key must contain 1 to 64 hexadecimal characters');
|
|
40
40
|
}
|
|
41
|
-
const normalized = privateKeyHex.toLowerCase().padStart(64,
|
|
42
|
-
const privateKey = hexToBytes(normalized,
|
|
41
|
+
const normalized = privateKeyHex.toLowerCase().padStart(64, '0');
|
|
42
|
+
const privateKey = hexToBytes(normalized, 'secp256k1 private key');
|
|
43
43
|
if (!secp256k1.utils.isValidSecretKey(privateKey)) {
|
|
44
|
-
throw new Error(
|
|
44
|
+
throw new Error('secp256k1 private key is outside the valid scalar range');
|
|
45
45
|
}
|
|
46
46
|
return normalized;
|
|
47
47
|
}
|
|
@@ -56,24 +56,23 @@ export function isValidSecp256k1PrivateKey(privateKeyHex) {
|
|
|
56
56
|
}
|
|
57
57
|
}
|
|
58
58
|
function parsePrivateKey(privateKeyHex) {
|
|
59
|
-
return hexToBytes(normalizeSecp256k1PrivateKey(privateKeyHex),
|
|
59
|
+
return hexToBytes(normalizeSecp256k1PrivateKey(privateKeyHex), 'secp256k1 private key');
|
|
60
60
|
}
|
|
61
61
|
function parsePublicKey(publicKeyHex) {
|
|
62
|
-
if (typeof publicKeyHex !==
|
|
63
|
-
(!COMPRESSED_PUBLIC_KEY.test(publicKeyHex) &&
|
|
64
|
-
|
|
65
|
-
throw new Error("secp256k1 public key must be a compressed or uncompressed SEC1 hex key");
|
|
62
|
+
if (typeof publicKeyHex !== 'string' ||
|
|
63
|
+
(!COMPRESSED_PUBLIC_KEY.test(publicKeyHex) && !UNCOMPRESSED_PUBLIC_KEY.test(publicKeyHex))) {
|
|
64
|
+
throw new Error('secp256k1 public key must be a compressed or uncompressed SEC1 hex key');
|
|
66
65
|
}
|
|
67
|
-
const publicKey = hexToBytes(publicKeyHex,
|
|
66
|
+
const publicKey = hexToBytes(publicKeyHex, 'secp256k1 public key');
|
|
68
67
|
// Parsing validates the SEC1 prefix, coordinate range, and curve equation.
|
|
69
68
|
secp256k1.Point.fromBytes(publicKey);
|
|
70
69
|
return publicKey;
|
|
71
70
|
}
|
|
72
71
|
function parseDigest(digestHex) {
|
|
73
|
-
if (typeof digestHex !==
|
|
74
|
-
throw new Error(
|
|
72
|
+
if (typeof digestHex !== 'string' || digestHex.length !== 64) {
|
|
73
|
+
throw new Error('secp256k1 digest must be exactly 32 bytes of hexadecimal data');
|
|
75
74
|
}
|
|
76
|
-
return hexToBytes(digestHex,
|
|
75
|
+
return hexToBytes(digestHex, 'secp256k1 digest');
|
|
77
76
|
}
|
|
78
77
|
/** True for a valid compressed or uncompressed SEC1 secp256k1 public key. */
|
|
79
78
|
export function isValidSecp256k1PublicKey(publicKeyHex) {
|
|
@@ -113,17 +112,17 @@ export function signSecp256k1Digest(privateKeyHex, digestHex, options = {}) {
|
|
|
113
112
|
const signature = secp256k1.sign(parseDigest(digestHex), parsePrivateKey(privateKeyHex), {
|
|
114
113
|
lowS: options.lowS ?? false,
|
|
115
114
|
});
|
|
116
|
-
return signature.toHex(
|
|
115
|
+
return signature.toHex('der');
|
|
117
116
|
}
|
|
118
117
|
/** Verify a DER-encoded ECDSA signature, accepting historical high-S signatures. */
|
|
119
118
|
export function verifySecp256k1Digest(publicKeyHex, digestHex, signatureDerHex) {
|
|
120
119
|
const publicKey = parsePublicKey(publicKeyHex);
|
|
121
120
|
const digest = parseDigest(digestHex);
|
|
122
|
-
const signature = hexToBytes(signatureDerHex,
|
|
121
|
+
const signature = hexToBytes(signatureDerHex, 'secp256k1 DER signature');
|
|
123
122
|
// Parse once up front so malformed or non-DER input is rejected explicitly.
|
|
124
|
-
secp256k1.Signature.fromBytes(signature,
|
|
123
|
+
secp256k1.Signature.fromBytes(signature, 'der');
|
|
125
124
|
return secp256k1.verify(signature, digest, publicKey, {
|
|
126
|
-
format:
|
|
125
|
+
format: 'der',
|
|
127
126
|
lowS: false,
|
|
128
127
|
});
|
|
129
128
|
}
|
|
@@ -137,24 +136,24 @@ export function verifySecp256k1Digest(publicKeyHex, digestHex, signatureDerHex)
|
|
|
137
136
|
export function tweakAddSecp256k1PrivateKey(privateKeyHex, tweakHex) {
|
|
138
137
|
const n = secp256k1.Point.Fn.ORDER;
|
|
139
138
|
const key = BigInt(`0x${normalizeSecp256k1PrivateKey(privateKeyHex)}`);
|
|
140
|
-
if (typeof tweakHex !==
|
|
141
|
-
throw new Error(
|
|
139
|
+
if (typeof tweakHex !== 'string' || tweakHex.length !== 64 || !HEX.test(tweakHex)) {
|
|
140
|
+
throw new Error('secp256k1 tweak must be exactly 32 bytes of hexadecimal data');
|
|
142
141
|
}
|
|
143
142
|
const tweak = BigInt(`0x${tweakHex}`);
|
|
144
143
|
if (tweak >= n) {
|
|
145
|
-
throw new Error(
|
|
144
|
+
throw new Error('secp256k1 tweak is not below the curve order');
|
|
146
145
|
}
|
|
147
146
|
const child = (key + tweak) % n;
|
|
148
147
|
if (child === 0n) {
|
|
149
|
-
throw new Error(
|
|
148
|
+
throw new Error('secp256k1 tweaked private key is zero');
|
|
150
149
|
}
|
|
151
|
-
return child.toString(16).padStart(64,
|
|
150
|
+
return child.toString(16).padStart(64, '0');
|
|
152
151
|
}
|
|
153
152
|
/** Derive the fixed-width 32-byte ECDH x-coordinate shared secret. */
|
|
154
153
|
export function deriveSecp256k1SharedSecret(privateKeyHex, publicKeyHex) {
|
|
155
154
|
const encodedPoint = secp256k1.getSharedSecret(parsePrivateKey(privateKeyHex), parsePublicKey(publicKeyHex), true);
|
|
156
155
|
if (encodedPoint.length !== 33) {
|
|
157
|
-
throw new Error(
|
|
156
|
+
throw new Error('secp256k1 ECDH returned an unexpected point encoding');
|
|
158
157
|
}
|
|
159
158
|
return encodedPoint.slice(1);
|
|
160
159
|
}
|
package/dist/esm/sign.js
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
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
|
+
export { signEnvelope, signMessage, verifyEnvelopeSignature, verifySignature, } from './envelope/sign.js';
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* engine owns the verification + ordering policy.
|
|
10
10
|
*
|
|
11
11
|
* Storage and identity are both injected, so the engine has zero knowledge of
|
|
12
|
-
*
|
|
12
|
+
* the database, Oxy DIDs, or any app's lexicon — exactly what makes it reusable.
|
|
13
13
|
*/
|
|
14
14
|
import type { SignedRecordEnvelope } from '@oxy.so/contracts';
|
|
15
15
|
import type { VerificationMethodResolver } from '../identity/resolver';
|
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
* The engine ({@link ./verify}, {@link ./engine}) is storage-agnostic: it owns
|
|
5
5
|
* the verification state machine and continuity logic, and delegates EVERY read
|
|
6
6
|
* and write to an injected {@link RecordStore}. An app supplies a store over its
|
|
7
|
-
* own backend (oxy-api over
|
|
8
|
-
* Mention over its own
|
|
7
|
+
* own backend (oxy-api over Postgres `signed_records`/`repo_heads`, a node over
|
|
8
|
+
* SQLite, Mention over its own database) without the engine knowing anything Oxy- or
|
|
9
9
|
* app-specific.
|
|
10
10
|
*
|
|
11
11
|
* All methods are **subject-keyed**: `subject` is the chain's subject DID
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
* `append` MUST be atomic (record insert + head advance in one unit) and MUST
|
|
18
18
|
* translate a duplicate-key collision on the unique `(subject, seq)` /
|
|
19
19
|
* `recordId` index — i.e. a concurrent writer that already took this `seq` — into
|
|
20
|
-
* `{ ok: false, reason: 'chain_conflict' }` (
|
|
20
|
+
* `{ ok: false, reason: 'chain_conflict' }` (Postgres `23505` / SQLite
|
|
21
21
|
* `SQLITE_CONSTRAINT`). That is the real multi-writer race guard; the engine's
|
|
22
22
|
* pre-append continuity check is only the fast-path rejection.
|
|
23
23
|
*/
|
|
@@ -10,7 +10,8 @@
|
|
|
10
10
|
* - the {@link RecordStore} owns the monotonicity frontier + the chain head.
|
|
11
11
|
*
|
|
12
12
|
* The ordered checks (first failure wins):
|
|
13
|
-
* 1. **shape** — the base `
|
|
13
|
+
* 1. **shape** — the base `signedRecordEnvelopeCheck`, the Zod-free twin of
|
|
14
|
+
* `signedRecordEnvelopeSchema` (open `type`, opaque
|
|
14
15
|
* `record`). An app re-narrows `type` to its own set in its adapter.
|
|
15
16
|
* 2. **signature** — `verifyEnvelopeSignature` recomputes the canonical signing
|
|
16
17
|
* input and checks the secp256k1 signature against the embedded `publicKey`.
|
|
@@ -24,7 +25,7 @@
|
|
|
24
25
|
* subject?") is intentionally NOT here — it is an adapter-policy decision the
|
|
25
26
|
* caller makes before invoking the engine, not a property of the envelope.
|
|
26
27
|
*/
|
|
27
|
-
import {
|
|
28
|
+
import type { SignedRecordEnvelope } from '@oxy.so/contracts';
|
|
28
29
|
import { type VerificationMethodResolver } from '../identity/resolver';
|
|
29
30
|
import type { RecordStore } from './recordStore';
|
|
30
31
|
import type { VerifyOutcome } from './types';
|
|
@@ -0,0 +1,10 @@
|
|
|
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
|
+
export { signEnvelope, signMessage, verifyEnvelopeSignature, verifySignature, } from './envelope/sign';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@oxy.so/protocol",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.1",
|
|
4
4
|
"description": "Oxy Protocol — the app-agnostic base substrate: signed-record envelope, canonical JSON, signature/verification, and platform crypto. Reused by any Oxy app to decentralize its own content.",
|
|
5
5
|
"main": "dist/cjs/index.js",
|
|
6
6
|
"module": "dist/esm/index.js",
|
|
@@ -15,6 +15,9 @@
|
|
|
15
15
|
],
|
|
16
16
|
"random": [
|
|
17
17
|
"dist/types/random.d.ts"
|
|
18
|
+
],
|
|
19
|
+
"sign": [
|
|
20
|
+
"dist/types/sign.d.ts"
|
|
18
21
|
]
|
|
19
22
|
}
|
|
20
23
|
},
|
|
@@ -71,6 +74,18 @@
|
|
|
71
74
|
},
|
|
72
75
|
"default": "./dist/esm/random.js"
|
|
73
76
|
},
|
|
77
|
+
"./sign": {
|
|
78
|
+
"react-native": "./dist/esm/sign.js",
|
|
79
|
+
"import": {
|
|
80
|
+
"types": "./dist/types/sign.d.ts",
|
|
81
|
+
"default": "./dist/esm/sign.js"
|
|
82
|
+
},
|
|
83
|
+
"require": {
|
|
84
|
+
"types": "./dist/types/sign.d.ts",
|
|
85
|
+
"default": "./dist/cjs/sign.js"
|
|
86
|
+
},
|
|
87
|
+
"default": "./dist/esm/sign.js"
|
|
88
|
+
},
|
|
74
89
|
"./package.json": "./package.json"
|
|
75
90
|
},
|
|
76
91
|
"react-native": {
|
|
@@ -82,6 +97,7 @@
|
|
|
82
97
|
"files": [
|
|
83
98
|
"NOTICE",
|
|
84
99
|
"dist",
|
|
100
|
+
"!dist/**/*.tsbuildinfo",
|
|
85
101
|
"src"
|
|
86
102
|
],
|
|
87
103
|
"keywords": [
|
|
@@ -94,7 +110,7 @@
|
|
|
94
110
|
],
|
|
95
111
|
"repository": {
|
|
96
112
|
"type": "git",
|
|
97
|
-
"url": "https://github.com/
|
|
113
|
+
"url": "git+https://github.com/OxyHQ/oxy.git",
|
|
98
114
|
"directory": "packages/protocol"
|
|
99
115
|
},
|
|
100
116
|
"author": "OxyHQ",
|
|
@@ -112,26 +128,11 @@
|
|
|
112
128
|
"typescript": "tsc --noEmit",
|
|
113
129
|
"test": "jest --passWithNoTests",
|
|
114
130
|
"lint": "biome lint --error-on-warnings ./src",
|
|
115
|
-
"prepublishOnly": "node ../../scripts/assert-bun-publish.mjs && bun run clean && bun run build"
|
|
116
|
-
"release": "rm -rf dist && bun run build && release-it"
|
|
117
|
-
},
|
|
118
|
-
"release-it": {
|
|
119
|
-
"git": {
|
|
120
|
-
"tagName": "@oxy.so/protocol@${version}",
|
|
121
|
-
"tagAnnotation": "Release @oxy.so/protocol@${version}",
|
|
122
|
-
"commitMessage": "chore(protocol): release @oxy.so/protocol@${version}"
|
|
123
|
-
},
|
|
124
|
-
"github": {
|
|
125
|
-
"release": true,
|
|
126
|
-
"releaseName": "@oxy.so/protocol@${version}"
|
|
127
|
-
},
|
|
128
|
-
"npm": {
|
|
129
|
-
"publish": true
|
|
130
|
-
}
|
|
131
|
+
"prepublishOnly": "node ../../scripts/assert-bun-publish.mjs && bun run clean && bun run build"
|
|
131
132
|
},
|
|
132
133
|
"dependencies": {
|
|
133
134
|
"@noble/curves": "^1.9.7",
|
|
134
|
-
"@oxy.so/contracts": "^4.
|
|
135
|
+
"@oxy.so/contracts": "^4.24.0",
|
|
135
136
|
"zod": "^3.25.64"
|
|
136
137
|
},
|
|
137
138
|
"peerDependencies": {
|
|
@@ -156,17 +157,15 @@
|
|
|
156
157
|
}
|
|
157
158
|
},
|
|
158
159
|
"devDependencies": {
|
|
159
|
-
"@biomejs/biome": "^1.9.4",
|
|
160
160
|
"@react-native-async-storage/async-storage": "^2.2.0",
|
|
161
161
|
"@types/express": "^4.17.25",
|
|
162
162
|
"@types/node": "^22.20.1",
|
|
163
163
|
"@types/supertest": "^6.0.2",
|
|
164
164
|
"expo-crypto": "~56.0.3",
|
|
165
165
|
"expo-modules-core": "~57.0.17",
|
|
166
|
-
"expo-secure-store": "~
|
|
166
|
+
"expo-secure-store": "~57.0.4",
|
|
167
167
|
"express": "^4.22.2",
|
|
168
168
|
"jest": "^30.5.1",
|
|
169
|
-
"release-it": "^19.0.6",
|
|
170
169
|
"supertest": "^7.0.0",
|
|
171
170
|
"ts-jest": "^29.4.12",
|
|
172
171
|
"typescript": "^5.9.2"
|
|
@@ -26,9 +26,7 @@ describe('canonicalize', () => {
|
|
|
26
26
|
z: { y: 1, x: 2 },
|
|
27
27
|
a: { c: 3, b: { e: 5, d: 4 } },
|
|
28
28
|
};
|
|
29
|
-
expect(canonicalize(value)).toBe(
|
|
30
|
-
'{"a":{"b":{"d":4,"e":5},"c":3},"z":{"x":2,"y":1}}',
|
|
31
|
-
);
|
|
29
|
+
expect(canonicalize(value)).toBe('{"a":{"b":{"d":4,"e":5},"c":3},"z":{"x":2,"y":1}}');
|
|
32
30
|
});
|
|
33
31
|
|
|
34
32
|
it('is order-insensitive across deep nesting', () => {
|
|
@@ -91,9 +89,7 @@ describe('canonicalize', () => {
|
|
|
91
89
|
it('respects toJSON (Date and its ISO string canonicalize identically)', () => {
|
|
92
90
|
const date = new Date('2026-06-26T00:00:00.000Z');
|
|
93
91
|
expect(canonicalize(date)).toBe(JSON.stringify(date.toISOString()));
|
|
94
|
-
expect(canonicalize({ at: date })).toBe(
|
|
95
|
-
canonicalize({ at: '2026-06-26T00:00:00.000Z' }),
|
|
96
|
-
);
|
|
92
|
+
expect(canonicalize({ at: date })).toBe(canonicalize({ at: '2026-06-26T00:00:00.000Z' }));
|
|
97
93
|
});
|
|
98
94
|
});
|
|
99
95
|
|
|
@@ -80,11 +80,22 @@ function stubStore(overrides: Partial<RecordStore> = {}): RecordStore {
|
|
|
80
80
|
const FAR_FUTURE_NOW = 1_700_000_000_000 + 1_000;
|
|
81
81
|
|
|
82
82
|
describe('checkContinuity', () => {
|
|
83
|
-
const head = (headRecordId: string, seq: number): ChainHead => ({
|
|
83
|
+
const head = (headRecordId: string, seq: number): ChainHead => ({
|
|
84
|
+
headRecordId,
|
|
85
|
+
seq,
|
|
86
|
+
recordCount: seq + 1,
|
|
87
|
+
});
|
|
84
88
|
|
|
85
89
|
it('accepts a v1 record (unchained) regardless of head', async () => {
|
|
86
90
|
const env = await signEnvelope(
|
|
87
|
-
{
|
|
91
|
+
{
|
|
92
|
+
version: 1,
|
|
93
|
+
type: 'identity',
|
|
94
|
+
subject: SUBJECT,
|
|
95
|
+
issuer: SUBJECT,
|
|
96
|
+
record: { a: 1 },
|
|
97
|
+
issuedAt: 1,
|
|
98
|
+
},
|
|
88
99
|
PRIVATE_KEY,
|
|
89
100
|
);
|
|
90
101
|
expect(checkContinuity(head('a'.repeat(64), 3), env)).toEqual({ ok: true });
|
|
@@ -102,7 +113,10 @@ describe('checkContinuity', () => {
|
|
|
102
113
|
|
|
103
114
|
it('rejects a wrong prev as chain_fork', async () => {
|
|
104
115
|
const env = await signEnvelope(v2Fields({ seq: 1, prev: 'b'.repeat(64) }), PRIVATE_KEY);
|
|
105
|
-
expect(checkContinuity(head('a'.repeat(64), 0), env)).toEqual({
|
|
116
|
+
expect(checkContinuity(head('a'.repeat(64), 0), env)).toEqual({
|
|
117
|
+
ok: false,
|
|
118
|
+
reason: 'chain_fork',
|
|
119
|
+
});
|
|
106
120
|
});
|
|
107
121
|
|
|
108
122
|
it('rejects a seq gap (correct prev) as bad_seq', async () => {
|
|
@@ -157,7 +171,9 @@ describe('isAuthorizedKey', () => {
|
|
|
157
171
|
describe('verifyEnvelope', () => {
|
|
158
172
|
it('accepts a fresh, well-signed genesis record', async () => {
|
|
159
173
|
const env = await signEnvelope(v2Fields(), PRIVATE_KEY);
|
|
160
|
-
await expect(
|
|
174
|
+
await expect(
|
|
175
|
+
verifyEnvelope(stubStore(), resolver(SELF_RESOLVED), env, { now: FAR_FUTURE_NOW }),
|
|
176
|
+
).resolves.toEqual({
|
|
161
177
|
ok: true,
|
|
162
178
|
});
|
|
163
179
|
});
|
|
@@ -186,7 +202,9 @@ describe('verifyEnvelope', () => {
|
|
|
186
202
|
const stripped = { ...env };
|
|
187
203
|
delete (stripped as Record<string, unknown>)[field];
|
|
188
204
|
await expect(
|
|
189
|
-
verifyEnvelope(stubStore(), resolver(SELF_RESOLVED), stripped as SignedRecordEnvelope, {
|
|
205
|
+
verifyEnvelope(stubStore(), resolver(SELF_RESOLVED), stripped as SignedRecordEnvelope, {
|
|
206
|
+
now: FAR_FUTURE_NOW,
|
|
207
|
+
}),
|
|
190
208
|
).resolves.toEqual({ ok: false, reason: 'invalid_envelope' });
|
|
191
209
|
},
|
|
192
210
|
);
|
|
@@ -203,7 +221,9 @@ describe('verifyEnvelope', () => {
|
|
|
203
221
|
it('rejects an unauthorized key as public_key_not_a_current_verification_method', async () => {
|
|
204
222
|
const env = await signEnvelope(v2Fields(), PRIVATE_KEY);
|
|
205
223
|
await expect(
|
|
206
|
-
verifyEnvelope(stubStore(), resolver({ currentPublicKeys: ['cafe'] }), env, {
|
|
224
|
+
verifyEnvelope(stubStore(), resolver({ currentPublicKeys: ['cafe'] }), env, {
|
|
225
|
+
now: FAR_FUTURE_NOW,
|
|
226
|
+
}),
|
|
207
227
|
).resolves.toEqual({ ok: false, reason: 'public_key_not_a_current_verification_method' });
|
|
208
228
|
});
|
|
209
229
|
|
|
@@ -224,7 +244,9 @@ describe('verifyEnvelope', () => {
|
|
|
224
244
|
it('rejects an issuedAt not newer than the latest stored record as stale_issued_at', async () => {
|
|
225
245
|
const env = await signEnvelope(v2Fields({ issuedAt: 1_700_000_000_000 }), PRIVATE_KEY);
|
|
226
246
|
const store = stubStore({ latestIssuedAtForKey: async () => 1_700_000_000_000 });
|
|
227
|
-
await expect(
|
|
247
|
+
await expect(
|
|
248
|
+
verifyEnvelope(store, resolver(SELF_RESOLVED), env, { now: FAR_FUTURE_NOW }),
|
|
249
|
+
).resolves.toEqual({
|
|
228
250
|
ok: false,
|
|
229
251
|
reason: 'stale_issued_at',
|
|
230
252
|
});
|
|
@@ -249,7 +271,9 @@ describe('verifyAndAppend', () => {
|
|
|
249
271
|
}),
|
|
250
272
|
);
|
|
251
273
|
const store = stubStore({ append });
|
|
252
|
-
const result = await verifyAndAppend(store, resolver(SELF_RESOLVED), env, {
|
|
274
|
+
const result = await verifyAndAppend(store, resolver(SELF_RESOLVED), env, {
|
|
275
|
+
now: FAR_FUTURE_NOW,
|
|
276
|
+
});
|
|
253
277
|
expect(result.ok).toBe(true);
|
|
254
278
|
expect(append).toHaveBeenCalledTimes(1);
|
|
255
279
|
const [subject, passedEnv, recordId] = append.mock.calls[0];
|
|
@@ -260,10 +284,15 @@ describe('verifyAndAppend', () => {
|
|
|
260
284
|
});
|
|
261
285
|
|
|
262
286
|
it('does NOT append when verification fails', async () => {
|
|
263
|
-
const tampered = {
|
|
287
|
+
const tampered = {
|
|
288
|
+
...(await signEnvelope(v2Fields(), PRIVATE_KEY)),
|
|
289
|
+
record: { x: 'tampered' },
|
|
290
|
+
};
|
|
264
291
|
const append = jest.fn();
|
|
265
292
|
const store = stubStore({ append });
|
|
266
|
-
const result = await verifyAndAppend(store, resolver(SELF_RESOLVED), tampered, {
|
|
293
|
+
const result = await verifyAndAppend(store, resolver(SELF_RESOLVED), tampered, {
|
|
294
|
+
now: FAR_FUTURE_NOW,
|
|
295
|
+
});
|
|
267
296
|
expect(result).toEqual({ ok: false, reason: 'bad_signature' });
|
|
268
297
|
expect(append).not.toHaveBeenCalled();
|
|
269
298
|
});
|
|
@@ -271,7 +300,9 @@ describe('verifyAndAppend', () => {
|
|
|
271
300
|
it('surfaces the store chain_conflict backstop', async () => {
|
|
272
301
|
const env = await signEnvelope(v2Fields(), PRIVATE_KEY);
|
|
273
302
|
const store = stubStore({ append: async () => ({ ok: false, reason: 'chain_conflict' }) });
|
|
274
|
-
await expect(
|
|
303
|
+
await expect(
|
|
304
|
+
verifyAndAppend(store, resolver(SELF_RESOLVED), env, { now: FAR_FUTURE_NOW }),
|
|
305
|
+
).resolves.toEqual({
|
|
275
306
|
ok: false,
|
|
276
307
|
reason: 'chain_conflict',
|
|
277
308
|
});
|
|
@@ -9,7 +9,9 @@
|
|
|
9
9
|
const PUBLIC_KEY = `04${'ab'.repeat(64)}`;
|
|
10
10
|
|
|
11
11
|
function mockNative(native: unknown): void {
|
|
12
|
-
jest.doMock('expo-modules-core', () => ({ requireOptionalNativeModule: () => native }), {
|
|
12
|
+
jest.doMock('expo-modules-core', () => ({ requireOptionalNativeModule: () => native }), {
|
|
13
|
+
virtual: true,
|
|
14
|
+
});
|
|
13
15
|
}
|
|
14
16
|
|
|
15
17
|
async function loadBridge() {
|
|
@@ -102,7 +104,10 @@ describe('tweakAddSecp256k1PrivateKey', () => {
|
|
|
102
104
|
expect(() => tweakAddSecp256k1PrivateKey(nMinusOne, `${'0'.repeat(63)}1`)).toThrow('zero');
|
|
103
105
|
// A tweak at or above n is invalid.
|
|
104
106
|
expect(() =>
|
|
105
|
-
tweakAddSecp256k1PrivateKey(
|
|
107
|
+
tweakAddSecp256k1PrivateKey(
|
|
108
|
+
'01',
|
|
109
|
+
'fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141',
|
|
110
|
+
),
|
|
106
111
|
).toThrow('curve order');
|
|
107
112
|
expect(() => tweakAddSecp256k1PrivateKey('01', 'zz')).toThrow();
|
|
108
113
|
});
|
|
@@ -19,7 +19,12 @@ function didDoc(overrides: Partial<DidDocument> = {}): DidDocument {
|
|
|
19
19
|
id: SUBJECT,
|
|
20
20
|
controller: [SUBJECT],
|
|
21
21
|
verificationMethod: [
|
|
22
|
-
{
|
|
22
|
+
{
|
|
23
|
+
id: `${SUBJECT}#key-1`,
|
|
24
|
+
type: 'EcdsaSecp256k1VerificationKey2019',
|
|
25
|
+
controller: SUBJECT,
|
|
26
|
+
publicKeyHex: KEY_A,
|
|
27
|
+
},
|
|
23
28
|
],
|
|
24
29
|
authentication: [`${SUBJECT}#key-1`],
|
|
25
30
|
assertionMethod: [`${SUBJECT}#key-1`],
|
|
@@ -50,7 +55,9 @@ describe('didWebToUrl', () => {
|
|
|
50
55
|
});
|
|
51
56
|
|
|
52
57
|
it('decodes a %3A port in the host segment', () => {
|
|
53
|
-
expect(didWebToUrl('did:web:localhost%3A3000')).toBe(
|
|
58
|
+
expect(didWebToUrl('did:web:localhost%3A3000')).toBe(
|
|
59
|
+
'https://localhost:3000/.well-known/did.json',
|
|
60
|
+
);
|
|
54
61
|
});
|
|
55
62
|
|
|
56
63
|
it('returns null for a non-did:web identifier', () => {
|
|
@@ -75,8 +82,18 @@ describe('createDidWebResolver', () => {
|
|
|
75
82
|
it('collects every assertionMethod key (deduped)', async () => {
|
|
76
83
|
const doc = didDoc({
|
|
77
84
|
verificationMethod: [
|
|
78
|
-
{
|
|
79
|
-
|
|
85
|
+
{
|
|
86
|
+
id: `${SUBJECT}#key-1`,
|
|
87
|
+
type: 'EcdsaSecp256k1VerificationKey2019',
|
|
88
|
+
controller: SUBJECT,
|
|
89
|
+
publicKeyHex: KEY_A,
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
id: `${SUBJECT}#key-2`,
|
|
93
|
+
type: 'EcdsaSecp256k1VerificationKey2019',
|
|
94
|
+
controller: SUBJECT,
|
|
95
|
+
publicKeyHex: KEY_B,
|
|
96
|
+
},
|
|
80
97
|
],
|
|
81
98
|
assertionMethod: [`${SUBJECT}#key-1`, `${SUBJECT}#key-2`],
|
|
82
99
|
});
|
|
@@ -90,8 +107,18 @@ describe('createDidWebResolver', () => {
|
|
|
90
107
|
// the hex signing key only.
|
|
91
108
|
const doc = didDoc({
|
|
92
109
|
verificationMethod: [
|
|
93
|
-
{
|
|
94
|
-
|
|
110
|
+
{
|
|
111
|
+
id: `${SUBJECT}#key-1`,
|
|
112
|
+
type: 'EcdsaSecp256k1VerificationKey2019',
|
|
113
|
+
controller: SUBJECT,
|
|
114
|
+
publicKeyHex: KEY_A,
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
id: `${SUBJECT}#atproto`,
|
|
118
|
+
type: 'Multikey',
|
|
119
|
+
controller: SUBJECT,
|
|
120
|
+
publicKeyMultibase: 'zQ3shFakeMultibaseKey',
|
|
121
|
+
},
|
|
95
122
|
],
|
|
96
123
|
authentication: [`${SUBJECT}#key-1`, `${SUBJECT}#atproto`],
|
|
97
124
|
assertionMethod: [`${SUBJECT}#key-1`, `${SUBJECT}#atproto`],
|
|
@@ -113,7 +140,9 @@ describe('createDidWebResolver', () => {
|
|
|
113
140
|
});
|
|
114
141
|
|
|
115
142
|
it('returns null when the document id does not match the subject', async () => {
|
|
116
|
-
const resolver = createDidWebResolver(async () =>
|
|
143
|
+
const resolver = createDidWebResolver(async () =>
|
|
144
|
+
jsonResponse(didDoc({ id: 'did:web:other.example' })),
|
|
145
|
+
);
|
|
117
146
|
expect(await resolver.resolve(SUBJECT)).toBeNull();
|
|
118
147
|
});
|
|
119
148
|
|