@oxy.so/protocol 1.2.2 → 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 +18 -4
- 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
|
@@ -26,9 +26,7 @@ import { dirname, join, resolve } from 'node:path';
|
|
|
26
26
|
const SRC_DIR = resolve(__dirname, '..');
|
|
27
27
|
const ROOT_ENTRY = join(SRC_DIR, 'index.ts');
|
|
28
28
|
|
|
29
|
-
const packageJson = JSON.parse(
|
|
30
|
-
readFileSync(resolve(SRC_DIR, '..', 'package.json'), 'utf8'),
|
|
31
|
-
) as {
|
|
29
|
+
const packageJson = JSON.parse(readFileSync(resolve(SRC_DIR, '..', 'package.json'), 'utf8')) as {
|
|
32
30
|
peerDependenciesMeta?: Record<string, { optional?: boolean }>;
|
|
33
31
|
};
|
|
34
32
|
|
|
@@ -218,9 +216,13 @@ describe('crypto.native optional-peer degradation', () => {
|
|
|
218
216
|
});
|
|
219
217
|
jest.doMock('expo-crypto', () => fakeCrypto, { virtual: true });
|
|
220
218
|
jest.doMock('expo-secure-store', () => fakeSecureStore, { virtual: true });
|
|
221
|
-
jest.doMock(
|
|
222
|
-
|
|
223
|
-
|
|
219
|
+
jest.doMock(
|
|
220
|
+
'@react-native-async-storage/async-storage',
|
|
221
|
+
() => ({ default: fakeAsyncStorage }),
|
|
222
|
+
{
|
|
223
|
+
virtual: true,
|
|
224
|
+
},
|
|
225
|
+
);
|
|
224
226
|
|
|
225
227
|
const mod = require(MODULE_PATH) as typeof import('../platform/crypto.native');
|
|
226
228
|
|
|
@@ -209,7 +209,8 @@ describe('createRateLimiter', () => {
|
|
|
209
209
|
await jest.isolateModulesAsync(async () => {
|
|
210
210
|
const reloaded = await import('../node/rateLimit');
|
|
211
211
|
afterRestart = reloaded.clientRateLimitKey({ ip: ADDRESS } as Request);
|
|
212
|
-
stableAfterRestart =
|
|
212
|
+
stableAfterRestart =
|
|
213
|
+
reloaded.clientRateLimitKey({ ip: ADDRESS } as Request) === afterRestart;
|
|
213
214
|
});
|
|
214
215
|
|
|
215
216
|
expect(afterRestart).not.toBe(before);
|
|
@@ -3,70 +3,68 @@ import {
|
|
|
3
3
|
meetsRegistrationPowDifficulty,
|
|
4
4
|
registrationPowMessage,
|
|
5
5
|
REGISTRATION_POW_DIFFICULTY_BITS,
|
|
6
|
-
} from
|
|
6
|
+
} from '../auth/registrationPow';
|
|
7
7
|
|
|
8
|
-
describe(
|
|
9
|
-
test(
|
|
10
|
-
expect(registrationPowMessage(
|
|
11
|
-
|
|
8
|
+
describe('registrationPowMessage', () => {
|
|
9
|
+
test('binds publicKey, timestamp and nonce, distinct from the registration signature message', () => {
|
|
10
|
+
expect(registrationPowMessage('pk', 1700000000000, '42')).toBe(
|
|
11
|
+
'oxy:register-pow:pk:1700000000000:42',
|
|
12
12
|
);
|
|
13
13
|
});
|
|
14
14
|
|
|
15
|
-
test(
|
|
16
|
-
expect(registrationPowMessage(
|
|
17
|
-
registrationPowMessage("pk", 1, "b"),
|
|
18
|
-
);
|
|
15
|
+
test('a different nonce is a different message', () => {
|
|
16
|
+
expect(registrationPowMessage('pk', 1, 'a')).not.toBe(registrationPowMessage('pk', 1, 'b'));
|
|
19
17
|
});
|
|
20
18
|
});
|
|
21
19
|
|
|
22
|
-
describe(
|
|
20
|
+
describe('countLeadingZeroBits', () => {
|
|
23
21
|
test.each([
|
|
24
|
-
[
|
|
25
|
-
[
|
|
26
|
-
[
|
|
27
|
-
[
|
|
28
|
-
[
|
|
29
|
-
[
|
|
30
|
-
[
|
|
31
|
-
[
|
|
32
|
-
[
|
|
33
|
-
[
|
|
34
|
-
[
|
|
22
|
+
['', 0],
|
|
23
|
+
['f', 0],
|
|
24
|
+
['8000', 0],
|
|
25
|
+
['0', 4],
|
|
26
|
+
['00', 8],
|
|
27
|
+
['01', 7],
|
|
28
|
+
['0f', 4],
|
|
29
|
+
['1f', 3],
|
|
30
|
+
['2f', 2],
|
|
31
|
+
['4f', 1],
|
|
32
|
+
['0000f', 16],
|
|
35
33
|
// Case-insensitive: the digest producers this compares against are
|
|
36
34
|
// lowercase, but the counter must not silently under-count an uppercase one.
|
|
37
|
-
[
|
|
38
|
-
])(
|
|
35
|
+
['00F', 8],
|
|
36
|
+
])('counts %s as %i leading zero bits', (hex, expected) => {
|
|
39
37
|
expect(countLeadingZeroBits(hex)).toBe(expected);
|
|
40
38
|
});
|
|
41
39
|
|
|
42
|
-
test(
|
|
43
|
-
expect(countLeadingZeroBits(`00f${
|
|
40
|
+
test('stops at the first non-zero nibble rather than scanning the whole digest', () => {
|
|
41
|
+
expect(countLeadingZeroBits(`00f${'0'.repeat(60)}`)).toBe(8);
|
|
44
42
|
});
|
|
45
43
|
|
|
46
|
-
test(
|
|
47
|
-
expect(countLeadingZeroBits(
|
|
48
|
-
expect(() => countLeadingZeroBits(
|
|
44
|
+
test('a non-hex character ends the count instead of throwing', () => {
|
|
45
|
+
expect(countLeadingZeroBits('0z')).toBe(4);
|
|
46
|
+
expect(() => countLeadingZeroBits('not-hex-at-all')).not.toThrow();
|
|
49
47
|
});
|
|
50
48
|
});
|
|
51
49
|
|
|
52
|
-
describe(
|
|
53
|
-
test(
|
|
54
|
-
expect(meetsRegistrationPowDifficulty(
|
|
55
|
-
expect(meetsRegistrationPowDifficulty(
|
|
50
|
+
describe('meetsRegistrationPowDifficulty', () => {
|
|
51
|
+
test('passes a digest that clears the bound', () => {
|
|
52
|
+
expect(meetsRegistrationPowDifficulty('00f0', 8)).toBe(true);
|
|
53
|
+
expect(meetsRegistrationPowDifficulty('00f0', 9)).toBe(false);
|
|
56
54
|
});
|
|
57
55
|
|
|
58
|
-
test(
|
|
59
|
-
expect(meetsRegistrationPowDifficulty(
|
|
56
|
+
test('an exact match at the bound passes', () => {
|
|
57
|
+
expect(meetsRegistrationPowDifficulty('00f0', 8)).toBe(true);
|
|
60
58
|
});
|
|
61
59
|
|
|
62
|
-
test(
|
|
60
|
+
test('the exported difficulty constant is a defensible, positive bound', () => {
|
|
63
61
|
// Pinned as a regression check on the constant itself: a change here is a
|
|
64
62
|
// deliberate retune, not an accidental one, and shows up in review.
|
|
65
63
|
expect(REGISTRATION_POW_DIFFICULTY_BITS).toBe(16);
|
|
66
|
-
expect(meetsRegistrationPowDifficulty(
|
|
64
|
+
expect(meetsRegistrationPowDifficulty('0'.repeat(64), REGISTRATION_POW_DIFFICULTY_BITS)).toBe(
|
|
67
65
|
true,
|
|
68
66
|
);
|
|
69
|
-
expect(meetsRegistrationPowDifficulty(
|
|
67
|
+
expect(meetsRegistrationPowDifficulty('f'.repeat(64), REGISTRATION_POW_DIFFICULTY_BITS)).toBe(
|
|
70
68
|
false,
|
|
71
69
|
);
|
|
72
70
|
});
|
|
@@ -8,111 +8,82 @@ import {
|
|
|
8
8
|
normalizeSecp256k1PublicKey,
|
|
9
9
|
signSecp256k1Digest,
|
|
10
10
|
verifySecp256k1Digest,
|
|
11
|
-
} from
|
|
11
|
+
} from '../secp256k1';
|
|
12
12
|
|
|
13
|
-
const PRIVATE_KEY_ONE = `${
|
|
14
|
-
const PRIVATE_KEY_TWO = `${
|
|
15
|
-
const DIGEST_ONE = `${
|
|
13
|
+
const PRIVATE_KEY_ONE = `${'0'.repeat(63)}1`;
|
|
14
|
+
const PRIVATE_KEY_TWO = `${'0'.repeat(63)}2`;
|
|
15
|
+
const DIGEST_ONE = `${'0'.repeat(63)}1`;
|
|
16
16
|
|
|
17
17
|
const PUBLIC_KEY_ONE =
|
|
18
|
-
|
|
19
|
-
|
|
18
|
+
'0479be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798' +
|
|
19
|
+
'483ada7726a3c4655da4fbfc0e1108a8fd17b448a68554199c47d08ffb10d4b8';
|
|
20
20
|
const COMPRESSED_PUBLIC_KEY_ONE =
|
|
21
|
-
|
|
21
|
+
'0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798';
|
|
22
22
|
const PUBLIC_KEY_TWO =
|
|
23
|
-
|
|
24
|
-
|
|
23
|
+
'04c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5' +
|
|
24
|
+
'1ae168fea63dc339a3c58419466ceaeef7f632653266d0e1236431a950cfe52a';
|
|
25
25
|
|
|
26
26
|
// Exact DER output produced by elliptic 6.6.1 for this RFC 6979 vector.
|
|
27
27
|
const HISTORICAL_HIGH_S_SIGNATURE =
|
|
28
|
-
|
|
29
|
-
|
|
28
|
+
'304502206673ffad2147741f04772b6f921f0ba6af0c1e77fc439e65c36dedf4092e8898' +
|
|
29
|
+
'022100b3e568e9ad1f52577fedf107fda18f5ebb8e5c220badf23532bf6fbcc67fb4b8';
|
|
30
30
|
const LOW_S_SIGNATURE =
|
|
31
|
-
|
|
32
|
-
|
|
31
|
+
'304402206673ffad2147741f04772b6f921f0ba6af0c1e77fc439e65c36dedf4092e8898' +
|
|
32
|
+
'02204c1a971652e0ada880120ef8025e709fff2080c4a39aae068d12eed009b68c89';
|
|
33
33
|
|
|
34
|
-
describe(
|
|
35
|
-
test(
|
|
36
|
-
expect(normalizeSecp256k1PrivateKey(
|
|
34
|
+
describe('secp256k1 primitives', () => {
|
|
35
|
+
test('preserves the canonical private/public key formats', () => {
|
|
36
|
+
expect(normalizeSecp256k1PrivateKey('1')).toBe(PRIVATE_KEY_ONE);
|
|
37
37
|
expect(deriveSecp256k1PublicKey(PRIVATE_KEY_ONE)).toBe(PUBLIC_KEY_ONE);
|
|
38
|
-
expect(deriveSecp256k1PublicKey(PRIVATE_KEY_ONE, true)).toBe(
|
|
39
|
-
|
|
40
|
-
);
|
|
41
|
-
expect(normalizeSecp256k1PublicKey(COMPRESSED_PUBLIC_KEY_ONE)).toBe(
|
|
42
|
-
PUBLIC_KEY_ONE,
|
|
43
|
-
);
|
|
44
|
-
expect(normalizeSecp256k1PublicKey(PUBLIC_KEY_ONE, true)).toBe(
|
|
45
|
-
COMPRESSED_PUBLIC_KEY_ONE,
|
|
46
|
-
);
|
|
38
|
+
expect(deriveSecp256k1PublicKey(PRIVATE_KEY_ONE, true)).toBe(COMPRESSED_PUBLIC_KEY_ONE);
|
|
39
|
+
expect(normalizeSecp256k1PublicKey(COMPRESSED_PUBLIC_KEY_ONE)).toBe(PUBLIC_KEY_ONE);
|
|
40
|
+
expect(normalizeSecp256k1PublicKey(PUBLIC_KEY_ONE, true)).toBe(COMPRESSED_PUBLIC_KEY_ONE);
|
|
47
41
|
});
|
|
48
42
|
|
|
49
|
-
test(
|
|
43
|
+
test('matches the historical deterministic DER wire format', () => {
|
|
50
44
|
const signature = signSecp256k1Digest(PRIVATE_KEY_ONE, DIGEST_ONE);
|
|
51
45
|
expect(signature).toBe(HISTORICAL_HIGH_S_SIGNATURE);
|
|
52
|
-
expect(verifySecp256k1Digest(PUBLIC_KEY_ONE, DIGEST_ONE, signature)).toBe(
|
|
53
|
-
|
|
54
|
-
);
|
|
55
|
-
expect(
|
|
56
|
-
verifySecp256k1Digest(COMPRESSED_PUBLIC_KEY_ONE, DIGEST_ONE, signature),
|
|
57
|
-
).toBe(true);
|
|
46
|
+
expect(verifySecp256k1Digest(PUBLIC_KEY_ONE, DIGEST_ONE, signature)).toBe(true);
|
|
47
|
+
expect(verifySecp256k1Digest(COMPRESSED_PUBLIC_KEY_ONE, DIGEST_ONE, signature)).toBe(true);
|
|
58
48
|
});
|
|
59
49
|
|
|
60
|
-
test(
|
|
50
|
+
test('supports explicit low-S signatures while accepting both historical forms', () => {
|
|
61
51
|
const signature = signSecp256k1Digest(PRIVATE_KEY_ONE, DIGEST_ONE, {
|
|
62
52
|
lowS: true,
|
|
63
53
|
});
|
|
64
54
|
expect(signature).toBe(LOW_S_SIGNATURE);
|
|
65
|
-
expect(verifySecp256k1Digest(PUBLIC_KEY_ONE, DIGEST_ONE, signature)).toBe(
|
|
55
|
+
expect(verifySecp256k1Digest(PUBLIC_KEY_ONE, DIGEST_ONE, signature)).toBe(true);
|
|
56
|
+
expect(verifySecp256k1Digest(PUBLIC_KEY_ONE, DIGEST_ONE, HISTORICAL_HIGH_S_SIGNATURE)).toBe(
|
|
66
57
|
true,
|
|
67
58
|
);
|
|
68
|
-
expect(
|
|
69
|
-
verifySecp256k1Digest(
|
|
70
|
-
PUBLIC_KEY_ONE,
|
|
71
|
-
DIGEST_ONE,
|
|
72
|
-
HISTORICAL_HIGH_S_SIGNATURE,
|
|
73
|
-
),
|
|
74
|
-
).toBe(true);
|
|
75
59
|
});
|
|
76
60
|
|
|
77
|
-
test(
|
|
78
|
-
const expected =
|
|
79
|
-
|
|
80
|
-
const
|
|
81
|
-
PRIVATE_KEY_ONE,
|
|
82
|
-
PUBLIC_KEY_TWO,
|
|
83
|
-
);
|
|
84
|
-
const twoToOne = deriveSecp256k1SharedSecret(
|
|
85
|
-
PRIVATE_KEY_TWO,
|
|
86
|
-
PUBLIC_KEY_ONE,
|
|
87
|
-
);
|
|
61
|
+
test('derives the fixed-width ECDH x-coordinate symmetrically', () => {
|
|
62
|
+
const expected = 'c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5';
|
|
63
|
+
const oneToTwo = deriveSecp256k1SharedSecret(PRIVATE_KEY_ONE, PUBLIC_KEY_TWO);
|
|
64
|
+
const twoToOne = deriveSecp256k1SharedSecret(PRIVATE_KEY_TWO, PUBLIC_KEY_ONE);
|
|
88
65
|
|
|
89
|
-
expect(Buffer.from(oneToTwo).toString(
|
|
66
|
+
expect(Buffer.from(oneToTwo).toString('hex')).toBe(expected);
|
|
90
67
|
expect(oneToTwo).toEqual(twoToOne);
|
|
91
68
|
expect(oneToTwo).toHaveLength(32);
|
|
92
69
|
});
|
|
93
70
|
|
|
94
|
-
test(
|
|
71
|
+
test('generates canonical key pairs that round-trip', () => {
|
|
95
72
|
const keyPair = generateSecp256k1KeyPair();
|
|
96
73
|
expect(keyPair.privateKey).toMatch(/^[0-9a-f]{64}$/);
|
|
97
74
|
expect(keyPair.publicKey).toMatch(/^04[0-9a-f]{128}$/);
|
|
98
|
-
expect(deriveSecp256k1PublicKey(keyPair.privateKey)).toBe(
|
|
99
|
-
keyPair.publicKey,
|
|
100
|
-
);
|
|
75
|
+
expect(deriveSecp256k1PublicKey(keyPair.privateKey)).toBe(keyPair.publicKey);
|
|
101
76
|
});
|
|
102
77
|
|
|
103
|
-
test(
|
|
104
|
-
expect(isValidSecp256k1PrivateKey(
|
|
105
|
-
expect(isValidSecp256k1PrivateKey(
|
|
106
|
-
expect(isValidSecp256k1PrivateKey(
|
|
107
|
-
expect(isValidSecp256k1PublicKey(`04${
|
|
108
|
-
expect(isValidSecp256k1PublicKey(`05${PUBLIC_KEY_ONE.slice(2)}`)).toBe(
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
expect(() => signSecp256k1Digest(PRIVATE_KEY_ONE, "00")).toThrow(
|
|
112
|
-
"digest must be exactly 32 bytes",
|
|
78
|
+
test('strictly rejects malformed scalars, points, digests, and DER', () => {
|
|
79
|
+
expect(isValidSecp256k1PrivateKey('0')).toBe(false);
|
|
80
|
+
expect(isValidSecp256k1PrivateKey('zz')).toBe(false);
|
|
81
|
+
expect(isValidSecp256k1PrivateKey('f'.repeat(66))).toBe(false);
|
|
82
|
+
expect(isValidSecp256k1PublicKey(`04${'0'.repeat(128)}`)).toBe(false);
|
|
83
|
+
expect(isValidSecp256k1PublicKey(`05${PUBLIC_KEY_ONE.slice(2)}`)).toBe(false);
|
|
84
|
+
expect(() => signSecp256k1Digest(PRIVATE_KEY_ONE, '00')).toThrow(
|
|
85
|
+
'digest must be exactly 32 bytes',
|
|
113
86
|
);
|
|
114
|
-
expect(() =>
|
|
115
|
-
verifySecp256k1Digest(PUBLIC_KEY_ONE, DIGEST_ONE, "00"),
|
|
116
|
-
).toThrow();
|
|
87
|
+
expect(() => verifySecp256k1Digest(PUBLIC_KEY_ONE, DIGEST_ONE, '00')).toThrow();
|
|
117
88
|
});
|
|
118
89
|
});
|
|
@@ -216,7 +216,10 @@ describe('verifyInclusionProof', () => {
|
|
|
216
216
|
const entries = heads(5);
|
|
217
217
|
const tree = await buildTransparencyTreeFromHeads(entries);
|
|
218
218
|
const proof = inclusionProof(tree, 2);
|
|
219
|
-
const tamperedLeaf = await transparencyLeafHash({
|
|
219
|
+
const tamperedLeaf = await transparencyLeafHash({
|
|
220
|
+
...entries[2],
|
|
221
|
+
headRecordId: 'f'.repeat(64),
|
|
222
|
+
});
|
|
220
223
|
await expect(
|
|
221
224
|
verifyInclusionProof({ leaf: tamperedLeaf, index: 2, treeSize: 5, proof, root: tree.root }),
|
|
222
225
|
).resolves.toBe(false);
|
|
@@ -89,7 +89,11 @@ const NIBBLE_LEADING_ZERO_BITS: Readonly<Record<string, number>> = {
|
|
|
89
89
|
* vice versa — the two are unrelated preimages even for the same
|
|
90
90
|
* `publicKey`/`timestamp` pair.
|
|
91
91
|
*/
|
|
92
|
-
export function registrationPowMessage(
|
|
92
|
+
export function registrationPowMessage(
|
|
93
|
+
publicKey: string,
|
|
94
|
+
timestamp: number,
|
|
95
|
+
nonce: string,
|
|
96
|
+
): string {
|
|
93
97
|
return `oxy:register-pow:${publicKey}:${timestamp}:${nonce}`;
|
|
94
98
|
}
|
|
95
99
|
|
package/src/chain/engine.ts
CHANGED
|
@@ -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
|
|
|
15
15
|
import type { SignedRecordEnvelope } from '@oxy.so/contracts';
|
package/src/chain/recordStore.ts
CHANGED
|
@@ -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
|
*/
|
package/src/chain/verify.ts
CHANGED
|
@@ -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`.
|
|
@@ -25,7 +26,8 @@
|
|
|
25
26
|
* caller makes before invoking the engine, not a property of the envelope.
|
|
26
27
|
*/
|
|
27
28
|
|
|
28
|
-
import {
|
|
29
|
+
import type { SignedRecordEnvelope } from '@oxy.so/contracts';
|
|
30
|
+
import { checkContract, signedRecordEnvelopeCheck } from '@oxy.so/contracts/client';
|
|
29
31
|
import { verifyEnvelopeSignature } from '../envelope/sign';
|
|
30
32
|
import { isAuthorizedKey, type VerificationMethodResolver } from '../identity/resolver';
|
|
31
33
|
import { checkContinuity } from './continuity';
|
|
@@ -56,7 +58,7 @@ export async function verifyEnvelope(
|
|
|
56
58
|
): Promise<VerifyOutcome> {
|
|
57
59
|
// 1. Base envelope shape (open `type`, `record` opaque). An app's adapter
|
|
58
60
|
// re-narrows `type` to its own accepted set before/around this call.
|
|
59
|
-
if (
|
|
61
|
+
if (checkContract(signedRecordEnvelopeCheck, env) === null) {
|
|
60
62
|
return { ok: false, reason: 'invalid_envelope' };
|
|
61
63
|
}
|
|
62
64
|
|
|
@@ -67,7 +69,12 @@ export async function verifyEnvelope(
|
|
|
67
69
|
}
|
|
68
70
|
|
|
69
71
|
// Enforce that v2 envelopes strictly contain required chain fields.
|
|
70
|
-
if (
|
|
72
|
+
if (
|
|
73
|
+
env.version === 2 &&
|
|
74
|
+
(typeof env.seq !== 'number' ||
|
|
75
|
+
typeof env.collection !== 'string' ||
|
|
76
|
+
typeof env.rkey !== 'string')
|
|
77
|
+
) {
|
|
71
78
|
return { ok: false, reason: 'invalid_envelope' };
|
|
72
79
|
}
|
|
73
80
|
|
package/src/envelope/recordId.ts
CHANGED
|
@@ -23,10 +23,7 @@ export async function sha256(message: string): Promise<string> {
|
|
|
23
23
|
// In React Native, use expo-crypto
|
|
24
24
|
if (isReactNative()) {
|
|
25
25
|
const Crypto = await loadExpoCrypto();
|
|
26
|
-
return Crypto.digestStringAsync(
|
|
27
|
-
Crypto.CryptoDigestAlgorithm.SHA256,
|
|
28
|
-
message,
|
|
29
|
-
);
|
|
26
|
+
return Crypto.digestStringAsync(Crypto.CryptoDigestAlgorithm.SHA256, message);
|
|
30
27
|
}
|
|
31
28
|
|
|
32
29
|
if (isNodeJS()) {
|
package/src/envelope/sign.ts
CHANGED
|
@@ -10,11 +10,7 @@
|
|
|
10
10
|
import type { SignedRecordEnvelope } from '@oxy.so/contracts';
|
|
11
11
|
import { signedRecordSigningInput, type SignedRecordSigningFields } from './signingInput';
|
|
12
12
|
import { sha256 } from './recordId';
|
|
13
|
-
import {
|
|
14
|
-
deriveSecp256k1PublicKey,
|
|
15
|
-
signSecp256k1Digest,
|
|
16
|
-
verifySecp256k1Digest,
|
|
17
|
-
} from '../secp256k1';
|
|
13
|
+
import { deriveSecp256k1PublicKey, signSecp256k1Digest, verifySecp256k1Digest } from '../secp256k1';
|
|
18
14
|
|
|
19
15
|
/** The one signature algorithm identifier the protocol emits. */
|
|
20
16
|
const ALG = 'ES256K-DER-SHA256' as const;
|
|
@@ -82,5 +78,9 @@ export async function signEnvelope(
|
|
|
82
78
|
* server / node owner check.
|
|
83
79
|
*/
|
|
84
80
|
export async function verifyEnvelopeSignature(envelope: SignedRecordEnvelope): Promise<boolean> {
|
|
85
|
-
return verifySignature(
|
|
81
|
+
return verifySignature(
|
|
82
|
+
signedRecordSigningInput(envelope),
|
|
83
|
+
envelope.signature,
|
|
84
|
+
envelope.publicKey,
|
|
85
|
+
);
|
|
86
86
|
}
|
|
@@ -42,7 +42,18 @@ export function signedRecordSigningInput(fields: SignedRecordSigningFields): str
|
|
|
42
42
|
const { version, type, subject, issuer, record, issuedAt } = fields;
|
|
43
43
|
if (version === 2) {
|
|
44
44
|
const { seq, prev, collection, rkey } = fields;
|
|
45
|
-
return canonicalize({
|
|
45
|
+
return canonicalize({
|
|
46
|
+
version,
|
|
47
|
+
type,
|
|
48
|
+
subject,
|
|
49
|
+
issuer,
|
|
50
|
+
record,
|
|
51
|
+
issuedAt,
|
|
52
|
+
seq,
|
|
53
|
+
prev,
|
|
54
|
+
collection,
|
|
55
|
+
rkey,
|
|
56
|
+
});
|
|
46
57
|
}
|
|
47
58
|
return canonicalize({ version, type, subject, issuer, record, issuedAt });
|
|
48
59
|
}
|
package/src/identity/resolver.ts
CHANGED
|
@@ -50,7 +50,13 @@ export interface VerificationMethodResolver {
|
|
|
50
50
|
/** Verdict of the key-authorization decision (a subset of {@link RejectionReason}). */
|
|
51
51
|
export type KeyAuthorization =
|
|
52
52
|
| { ok: true }
|
|
53
|
-
| {
|
|
53
|
+
| {
|
|
54
|
+
ok: false;
|
|
55
|
+
reason: Extract<
|
|
56
|
+
RejectionReason,
|
|
57
|
+
'public_key_not_a_current_verification_method' | 'untrusted_issuer'
|
|
58
|
+
>;
|
|
59
|
+
};
|
|
54
60
|
|
|
55
61
|
/**
|
|
56
62
|
* Decide whether `env`'s signing key (`env.publicKey`) is authorized for its
|
|
@@ -81,7 +87,8 @@ export function isAuthorizedKey(
|
|
|
81
87
|
resolved.custodialIssuer !== undefined &&
|
|
82
88
|
env.issuer === resolved.custodialIssuer
|
|
83
89
|
) {
|
|
84
|
-
return resolved.custodialPublicKey !== undefined &&
|
|
90
|
+
return resolved.custodialPublicKey !== undefined &&
|
|
91
|
+
env.publicKey === resolved.custodialPublicKey
|
|
85
92
|
? { ok: true }
|
|
86
93
|
: { ok: false, reason: 'public_key_not_a_current_verification_method' };
|
|
87
94
|
}
|
|
@@ -22,10 +22,7 @@ import {
|
|
|
22
22
|
type DidDocument,
|
|
23
23
|
type Secp256k1VerificationMethod,
|
|
24
24
|
} from '@oxy.so/contracts';
|
|
25
|
-
import type {
|
|
26
|
-
ResolvedVerificationMethods,
|
|
27
|
-
VerificationMethodResolver,
|
|
28
|
-
} from '../identity/resolver';
|
|
25
|
+
import type { ResolvedVerificationMethods, VerificationMethodResolver } from '../identity/resolver';
|
|
29
26
|
import { type NodeFetch, readBoundedJson } from './httpFetch';
|
|
30
27
|
import {
|
|
31
28
|
DEFAULT_CLIENT_MAX_REDIRECTS,
|
|
@@ -95,9 +92,7 @@ function isSecp256k1Vm(
|
|
|
95
92
|
*/
|
|
96
93
|
function collectCurrentPublicKeys(doc: DidDocument): string[] {
|
|
97
94
|
const byId = new Map(
|
|
98
|
-
doc.verificationMethod
|
|
99
|
-
.filter(isSecp256k1Vm)
|
|
100
|
-
.map((vm) => [vm.id, vm.publicKeyHex] as const),
|
|
95
|
+
doc.verificationMethod.filter(isSecp256k1Vm).map((vm) => [vm.id, vm.publicKeyHex] as const),
|
|
101
96
|
);
|
|
102
97
|
const keys: string[] = [];
|
|
103
98
|
for (const id of doc.assertionMethod) {
|