@peerbit/trusted-network 6.0.113 → 6.0.114
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/src/v2-authority-entry.d.ts +13 -17
- package/dist/src/v2-authority-entry.d.ts.map +1 -1
- package/dist/src/v2-authority-entry.js +78 -147
- package/dist/src/v2-authority-entry.js.map +1 -1
- package/dist/src/v2-policy-engine.d.ts.map +1 -1
- package/dist/src/v2-policy-engine.js +4 -42
- package/dist/src/v2-policy-engine.js.map +1 -1
- package/dist/src/v2-resource-fence-entry.d.ts.map +1 -1
- package/dist/src/v2-resource-fence-entry.js +115 -11
- package/dist/src/v2-resource-fence-entry.js.map +1 -1
- package/dist/src/v2.d.ts.map +1 -1
- package/dist/src/v2.js +22 -0
- package/dist/src/v2.js.map +1 -1
- package/package.json +4 -4
- package/src/v2-authority-entry.ts +110 -221
- package/src/v2-policy-engine.ts +7 -54
- package/src/v2-resource-fence-entry.ts +180 -10
- package/src/v2.ts +25 -0
|
@@ -1,3 +1,12 @@
|
|
|
1
|
+
import { serialize } from "@dao-xyz/borsh";
|
|
2
|
+
import {
|
|
3
|
+
calculateRawCid,
|
|
4
|
+
cidifyString,
|
|
5
|
+
codecMap,
|
|
6
|
+
defaultHasher,
|
|
7
|
+
stringifyCid,
|
|
8
|
+
} from "@peerbit/blocks-interface";
|
|
9
|
+
import { EntryType, type Meta } from "@peerbit/log";
|
|
1
10
|
import { equals } from "uint8arrays";
|
|
2
11
|
import {
|
|
3
12
|
type AuthenticatedAuthorityEntryV0V2,
|
|
@@ -16,10 +25,142 @@ import {
|
|
|
16
25
|
|
|
17
26
|
const RESOURCE_FENCE_AUTHORITY_LIMITS_V2 = Object.freeze({
|
|
18
27
|
maximumEntryBytes: TRUSTED_NETWORK_V2_MAX_RESOURCE_FENCE_ENTRY_BYTES,
|
|
19
|
-
maximumDirectParents: TRUSTED_NETWORK_V2_MAX_RESOURCE_FENCE_DIRECT_PARENTS,
|
|
20
|
-
exactPayloadBytes: TRUSTED_NETWORK_V2_RESOURCE_FENCE_BODY_BYTES,
|
|
21
28
|
});
|
|
22
29
|
|
|
30
|
+
const SECP256K1_SIGNATURE_TEXT_BYTES = 132;
|
|
31
|
+
const SECP256K1_LOW_S_MAX_HEX =
|
|
32
|
+
"7fffffffffffffffffffffffffffffff5d576e7357a4501ddfe92f46681b20a0";
|
|
33
|
+
|
|
34
|
+
const isLowerHexByteV2 = (value: number): boolean =>
|
|
35
|
+
(value >= 0x30 && value <= 0x39) || (value >= 0x61 && value <= 0x66);
|
|
36
|
+
|
|
37
|
+
const assertCanonicalSecp256k1SignatureV2 = (signature: Uint8Array): void => {
|
|
38
|
+
if (
|
|
39
|
+
signature.byteLength !== SECP256K1_SIGNATURE_TEXT_BYTES ||
|
|
40
|
+
signature[0] !== 0x30 ||
|
|
41
|
+
signature[1] !== 0x78
|
|
42
|
+
) {
|
|
43
|
+
throw new Error("Authority EntryV0 secp256k1 signature is not canonical");
|
|
44
|
+
}
|
|
45
|
+
for (let i = 2; i < signature.byteLength; i++) {
|
|
46
|
+
if (!isLowerHexByteV2(signature[i]!)) {
|
|
47
|
+
throw new Error("Authority EntryV0 secp256k1 signature is not canonical");
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
if (
|
|
51
|
+
signature[130] !== 0x31 ||
|
|
52
|
+
(signature[131] !== 0x62 && signature[131] !== 0x63)
|
|
53
|
+
) {
|
|
54
|
+
throw new Error("Authority EntryV0 secp256k1 signature is not canonical");
|
|
55
|
+
}
|
|
56
|
+
for (let i = 0; i < SECP256K1_LOW_S_MAX_HEX.length; i++) {
|
|
57
|
+
const actual = signature[66 + i]!;
|
|
58
|
+
const maximum = SECP256K1_LOW_S_MAX_HEX.charCodeAt(i);
|
|
59
|
+
if (actual < maximum) break;
|
|
60
|
+
if (actual > maximum) {
|
|
61
|
+
throw new Error("Authority EntryV0 secp256k1 signature is not canonical");
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const canonicalDirectParentsV2 = (
|
|
67
|
+
parents: string[],
|
|
68
|
+
): Array<{ cid: string; digest: Uint8Array }> => {
|
|
69
|
+
const seen = new Set<string>();
|
|
70
|
+
return parents.map((cid) => {
|
|
71
|
+
let parsed: ReturnType<typeof cidifyString>;
|
|
72
|
+
try {
|
|
73
|
+
parsed = cidifyString(cid);
|
|
74
|
+
} catch {
|
|
75
|
+
throw new Error(
|
|
76
|
+
"Authority EntryV0 direct parents must use canonical CIDv1/raw/sha2-256",
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
if (
|
|
80
|
+
!cid ||
|
|
81
|
+
parsed.version !== 1 ||
|
|
82
|
+
parsed.code !== codecMap.raw.code ||
|
|
83
|
+
parsed.multihash.code !== defaultHasher.code ||
|
|
84
|
+
parsed.multihash.digest.byteLength !== 32 ||
|
|
85
|
+
stringifyCid(parsed) !== cid
|
|
86
|
+
) {
|
|
87
|
+
throw new Error(
|
|
88
|
+
"Authority EntryV0 direct parents must use canonical CIDv1/raw/sha2-256",
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
if (seen.has(cid)) {
|
|
92
|
+
throw new Error("Authority EntryV0 direct parents must be unique");
|
|
93
|
+
}
|
|
94
|
+
seen.add(cid);
|
|
95
|
+
return {
|
|
96
|
+
cid,
|
|
97
|
+
digest: copyUint8ArrayWithLengthV2(
|
|
98
|
+
parsed.multihash.digest,
|
|
99
|
+
parsed.multihash.digest.byteLength,
|
|
100
|
+
),
|
|
101
|
+
};
|
|
102
|
+
});
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
type ResourceFenceEntryV0ProfileV2 = {
|
|
106
|
+
meta: Meta;
|
|
107
|
+
directParents: Array<{ cid: string; digest: Uint8Array }>;
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
const assertResourceFenceEntryV0ProfileV2 = (
|
|
111
|
+
authenticated: AuthenticatedAuthorityEntryV0V2,
|
|
112
|
+
): ResourceFenceEntryV0ProfileV2 => {
|
|
113
|
+
if (authenticated.hasHash) {
|
|
114
|
+
throw new Error(
|
|
115
|
+
"Authority EntryV0 has invalid resource hash option: embedded hash",
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
if (
|
|
119
|
+
authenticated.reservedBytes[0] !== 0 ||
|
|
120
|
+
authenticated.reservedBytes[1] !== 0 ||
|
|
121
|
+
authenticated.reservedBytes[2] !== 0 ||
|
|
122
|
+
authenticated.reservedBytes[3] !== 0
|
|
123
|
+
) {
|
|
124
|
+
throw new Error("Authority EntryV0 reserved bytes must be zero");
|
|
125
|
+
}
|
|
126
|
+
if (
|
|
127
|
+
authenticated.directParentCount >
|
|
128
|
+
TRUSTED_NETWORK_V2_MAX_RESOURCE_FENCE_DIRECT_PARENTS
|
|
129
|
+
) {
|
|
130
|
+
throw new Error(
|
|
131
|
+
`Authority EntryV0 may contain at most ${TRUSTED_NETWORK_V2_MAX_RESOURCE_FENCE_DIRECT_PARENTS} direct parents`,
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
if (
|
|
135
|
+
authenticated.payloadBytes.byteLength !==
|
|
136
|
+
TRUSTED_NETWORK_V2_RESOURCE_FENCE_BODY_BYTES
|
|
137
|
+
) {
|
|
138
|
+
throw new Error(
|
|
139
|
+
`Authority EntryV0 payload must contain exactly ${TRUSTED_NETWORK_V2_RESOURCE_FENCE_BODY_BYTES} bytes`,
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const meta = authenticated.entry.meta;
|
|
144
|
+
if (!equals(authenticated.metaBytes, serialize(meta))) {
|
|
145
|
+
throw new Error("Authority EntryV0 nested encoding is not canonical");
|
|
146
|
+
}
|
|
147
|
+
const authorityBytes = serialize(authenticated.descriptor.policyAuthority);
|
|
148
|
+
if (meta.type !== EntryType.APPEND) {
|
|
149
|
+
throw new Error("Authority EntryV0 must be an APPEND entry");
|
|
150
|
+
}
|
|
151
|
+
if (meta.next.length !== authenticated.directParentCount) {
|
|
152
|
+
throw new Error("Authority EntryV0 direct-parent framing is inconsistent");
|
|
153
|
+
}
|
|
154
|
+
if (!equals(meta.clock.id, authorityBytes)) {
|
|
155
|
+
throw new Error("Authority EntryV0 clock id is not the policy authority");
|
|
156
|
+
}
|
|
157
|
+
const signature = authenticated.entry.signatures[0]!;
|
|
158
|
+
if (serialize(signature.publicKey)[0] === 1) {
|
|
159
|
+
assertCanonicalSecp256k1SignatureV2(signature.signature);
|
|
160
|
+
}
|
|
161
|
+
return { meta, directParents: canonicalDirectParentsV2(meta.next) };
|
|
162
|
+
};
|
|
163
|
+
|
|
23
164
|
export type AuthenticatedResourceFenceEntryV2 = Readonly<{
|
|
24
165
|
entryBytes: Uint8Array;
|
|
25
166
|
entryCid: string;
|
|
@@ -53,14 +194,19 @@ const captureResourceIdV2 = (resourceId: Uint8Array): Uint8Array => {
|
|
|
53
194
|
const toResourceFenceTokenV2 = (
|
|
54
195
|
authenticated: AuthenticatedAuthorityEntryV0V2,
|
|
55
196
|
body: ResourceFenceV2,
|
|
197
|
+
entryCid: string,
|
|
198
|
+
digest: Uint8Array,
|
|
199
|
+
gid: string,
|
|
200
|
+
metaData: Uint8Array | undefined,
|
|
201
|
+
directParents: Array<{ cid: string; digest: Uint8Array }>,
|
|
56
202
|
): AuthenticatedResourceFenceEntryV2 => ({
|
|
57
203
|
entryBytes: authenticated.entryBytes,
|
|
58
|
-
entryCid
|
|
59
|
-
digest
|
|
204
|
+
entryCid,
|
|
205
|
+
digest,
|
|
60
206
|
body,
|
|
61
|
-
gid
|
|
62
|
-
metaData
|
|
63
|
-
directParents
|
|
207
|
+
gid,
|
|
208
|
+
metaData,
|
|
209
|
+
directParents,
|
|
64
210
|
});
|
|
65
211
|
|
|
66
212
|
/**
|
|
@@ -78,12 +224,19 @@ export const authenticateResourceFenceEntryV2 = async ({
|
|
|
78
224
|
throw new Error("Expected resource gid must be a string");
|
|
79
225
|
}
|
|
80
226
|
const capturedResourceId = captureResourceIdV2(expectedResourceId);
|
|
227
|
+
let profile: ResourceFenceEntryV0ProfileV2 | undefined;
|
|
81
228
|
const authenticated = await authenticateAuthorityEntryV0V2(
|
|
82
229
|
entryBytes,
|
|
83
230
|
descriptor,
|
|
84
|
-
RESOURCE_FENCE_AUTHORITY_LIMITS_V2,
|
|
231
|
+
RESOURCE_FENCE_AUTHORITY_LIMITS_V2.maximumEntryBytes,
|
|
232
|
+
(candidate) => {
|
|
233
|
+
profile = assertResourceFenceEntryV0ProfileV2(candidate);
|
|
234
|
+
},
|
|
85
235
|
);
|
|
86
|
-
if (
|
|
236
|
+
if (profile === undefined) {
|
|
237
|
+
throw new Error("Resource fence EntryV0 profile was not applied");
|
|
238
|
+
}
|
|
239
|
+
if (profile.meta.gid !== expectedGid) {
|
|
87
240
|
throw new Error("Resource fence belongs to another resource log");
|
|
88
241
|
}
|
|
89
242
|
const body = decodeResourceFenceV2(
|
|
@@ -93,5 +246,22 @@ export const authenticateResourceFenceEntryV2 = async ({
|
|
|
93
246
|
if (!equals(body.resourceId, capturedResourceId)) {
|
|
94
247
|
throw new Error("Resource fence belongs to another resource");
|
|
95
248
|
}
|
|
96
|
-
|
|
249
|
+
const prepared = await calculateRawCid(authenticated.entryBytes);
|
|
250
|
+
return toResourceFenceTokenV2(
|
|
251
|
+
authenticated,
|
|
252
|
+
body,
|
|
253
|
+
prepared.cid,
|
|
254
|
+
copyUint8ArrayWithLengthV2(
|
|
255
|
+
prepared.block.cid.multihash.digest,
|
|
256
|
+
prepared.block.cid.multihash.digest.byteLength,
|
|
257
|
+
),
|
|
258
|
+
profile.meta.gid,
|
|
259
|
+
profile.meta.data === undefined
|
|
260
|
+
? undefined
|
|
261
|
+
: copyUint8ArrayWithLengthV2(
|
|
262
|
+
profile.meta.data,
|
|
263
|
+
profile.meta.data.byteLength,
|
|
264
|
+
),
|
|
265
|
+
profile.directParents,
|
|
266
|
+
);
|
|
97
267
|
};
|
package/src/v2.ts
CHANGED
|
@@ -478,6 +478,31 @@ export const decodePolicySnapshotBodyV2 = (
|
|
|
478
478
|
bytes: Uint8Array,
|
|
479
479
|
descriptor: NetworkDescriptorV2,
|
|
480
480
|
): PolicySnapshotBodyV2 => {
|
|
481
|
+
const byteLength = exactUint8ArrayByteLengthV2(bytes);
|
|
482
|
+
// [2, 1] variant (2), network id (32), sequence (8), and previous
|
|
483
|
+
// digest (32) place the bindings vec<u32 LE> count at offset 74 and its
|
|
484
|
+
// elements at offset 78. Bound that count by captured input before Borsh can
|
|
485
|
+
// allocate from it. This is only a feasibility guard, not a member cap.
|
|
486
|
+
const bindingsCountOffset = 74;
|
|
487
|
+
const bindingsOffset = 78;
|
|
488
|
+
if (byteLength < bindingsOffset) {
|
|
489
|
+
throw new Error("Policy snapshot body has truncated bindings framing");
|
|
490
|
+
}
|
|
491
|
+
const bindingsCount =
|
|
492
|
+
bytes[bindingsCountOffset]! +
|
|
493
|
+
bytes[bindingsCountOffset + 1]! * 0x100 +
|
|
494
|
+
bytes[bindingsCountOffset + 2]! * 0x10000 +
|
|
495
|
+
bytes[bindingsCountOffset + 3]! * 0x1000000;
|
|
496
|
+
// An Ed25519 key variant and key consume 33 bytes; roles and the absent
|
|
497
|
+
// encryption-commitment option consume one byte each. This 35-byte minimum
|
|
498
|
+
// is structural, so it bounds allocation without imposing a policy size.
|
|
499
|
+
const minimumBindingBytes = 35;
|
|
500
|
+
if (
|
|
501
|
+
bindingsCount >
|
|
502
|
+
Math.floor((byteLength - bindingsOffset) / minimumBindingBytes)
|
|
503
|
+
) {
|
|
504
|
+
throw new Error("Policy snapshot body has impossible bindings count");
|
|
505
|
+
}
|
|
481
506
|
const body = deserialize(bytes, PolicySnapshotBodyV2);
|
|
482
507
|
assertCanonicalEncoding(bytes, body);
|
|
483
508
|
assertPolicySnapshotBodyV2(body, descriptor);
|