@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.
@@ -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: authenticated.entryCid,
59
- digest: authenticated.entryDigest,
204
+ entryCid,
205
+ digest,
60
206
  body,
61
- gid: authenticated.gid,
62
- metaData: authenticated.metaData,
63
- directParents: authenticated.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 (authenticated.gid !== expectedGid) {
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
- return toResourceFenceTokenV2(authenticated, body);
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);