this repo has no description
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Adds canonicalization to conform to the spec

orta 173eb615 1e469b3c

+103 -13
+67 -11
packages/verify/src/crypto/canonicalize.ts
··· 1 1 /** 2 - * Canonicalize an object for signing: sort keys recursively and JSON.stringify. 3 - * This ensures the same data always produces the same bytes for signing. 2 + * Canonicalize a value according to RFC 8785 (JSON Canonicalization Scheme). 3 + * https://datatracker.ietf.org/doc/html/rfc8785 4 + * 5 + * This ensures deterministic JSON output for cryptographic signing by: 6 + * - Sorting object keys by UTF-16 code units 7 + * - Serializing numbers per ES2020 Number.toString() (no -0, no NaN/Infinity) 8 + * - No optional whitespace 9 + * - Proper string escaping per RFC 8259 4 10 */ 5 - export function canonicalize(data: Record<string, unknown>): string { 6 - return JSON.stringify(sortKeys(data)); 11 + export function canonicalize(data: unknown): string { 12 + return serialize(data); 7 13 } 8 14 9 - function sortKeys(obj: unknown): unknown { 10 - if (obj === null || typeof obj !== "object") return obj; 11 - if (Array.isArray(obj)) return obj.map(sortKeys); 15 + function serialize(value: unknown): string { 16 + if (value === null) { 17 + return "null"; 18 + } 19 + 20 + switch (typeof value) { 21 + case "boolean": 22 + return value ? "true" : "false"; 23 + 24 + case "number": 25 + if (!Number.isFinite(value)) { 26 + throw new Error("RFC 8785: Cannot serialize Infinity or NaN"); 27 + } 28 + // RFC 8785 Section 3.2.2.3: -0 must be serialized as 0 29 + if (Object.is(value, -0)) { 30 + return "0"; 31 + } 32 + // Use ES Number.toString() which produces RFC 8785 compliant output 33 + return String(value); 12 34 13 - const sorted: Record<string, unknown> = {}; 14 - for (const key of Object.keys(obj as Record<string, unknown>).sort()) { 15 - sorted[key] = sortKeys((obj as Record<string, unknown>)[key]); 35 + case "string": 36 + // JSON.stringify handles RFC 8259 string escaping 37 + return JSON.stringify(value); 38 + 39 + case "object": 40 + if (Array.isArray(value)) { 41 + return "[" + value.map(serialize).join(",") + "]"; 42 + } 43 + return serializeObject(value as Record<string, unknown>); 44 + 45 + default: 46 + throw new Error(`RFC 8785: Cannot serialize value of type ${typeof value}`); 16 47 } 17 - return sorted; 48 + } 49 + 50 + /** 51 + * Serialize an object with keys sorted by UTF-16 code units per RFC 8785 Section 3.2.3. 52 + */ 53 + function serializeObject(obj: Record<string, unknown>): string { 54 + // RFC 8785: Sort keys by comparing UTF-16 code units 55 + const keys = Object.keys(obj).sort((a, b) => { 56 + const len = Math.min(a.length, b.length); 57 + for (let i = 0; i < len; i++) { 58 + const diff = a.charCodeAt(i) - b.charCodeAt(i); 59 + if (diff !== 0) return diff; 60 + } 61 + return a.length - b.length; 62 + }); 63 + 64 + const pairs: string[] = []; 65 + for (const key of keys) { 66 + const val = obj[key]; 67 + // Skip undefined values (not valid in JSON) 68 + if (val !== undefined) { 69 + pairs.push(JSON.stringify(key) + ":" + serialize(val)); 70 + } 71 + } 72 + 73 + return "{" + pairs.join(",") + "}"; 18 74 }
+36 -2
yarn.lock
··· 1906 1906 languageName: unknown 1907 1907 linkType: soft 1908 1908 1909 - "@keytrace/verify@workspace:packages/verify": 1909 + "@keytrace/verify@workspace:*, @keytrace/verify@workspace:packages/verify": 1910 1910 version: 0.0.0-use.local 1911 1911 resolution: "@keytrace/verify@workspace:packages/verify" 1912 1912 dependencies: ··· 6010 6010 languageName: node 6011 6011 linkType: hard 6012 6012 6013 - "esbuild@npm:^0.27.0, esbuild@npm:^0.27.2, esbuild@npm:^0.27.3": 6013 + "esbuild@npm:^0.27.0, esbuild@npm:^0.27.2, esbuild@npm:^0.27.3, esbuild@npm:~0.27.0": 6014 6014 version: 0.27.3 6015 6015 resolution: "esbuild@npm:0.27.3" 6016 6016 dependencies: ··· 6453 6453 version: 8.0.1 6454 6454 resolution: "get-stream@npm:8.0.1" 6455 6455 checksum: 10c0/5c2181e98202b9dae0bb4a849979291043e5892eb40312b47f0c22b9414fc9b28a3b6063d2375705eb24abc41ecf97894d9a51f64ff021511b504477b27b4290 6456 + languageName: node 6457 + linkType: hard 6458 + 6459 + "get-tsconfig@npm:^4.7.5": 6460 + version: 4.13.6 6461 + resolution: "get-tsconfig@npm:4.13.6" 6462 + dependencies: 6463 + resolve-pkg-maps: "npm:^1.0.0" 6464 + checksum: 10c0/bab6937302f542f97217cbe7cbbdfa7e85a56a377bc7a73e69224c1f0b7c9ae8365918e55752ae8648265903f506c1705f63c0de1d4bab1ec2830fef3e539a1a 6456 6465 languageName: node 6457 6466 linkType: hard 6458 6467 ··· 7206 7215 dependencies: 7207 7216 "@atproto/oauth-client-node": "npm:^0.3.0" 7208 7217 "@aws-sdk/client-s3": "npm:^3.700.0" 7218 + "@keytrace/verify": "workspace:*" 7209 7219 "@nuxtjs/google-fonts": "npm:^3.2.0" 7210 7220 "@nuxtjs/tailwindcss": "npm:^6.14.0" 7211 7221 lucide-vue-next: "npm:^0.563.0" 7212 7222 nuxt: "npm:^3.16.0" 7213 7223 reka-ui: "npm:^2.8.0" 7224 + tsx: "npm:^4.19.0" 7214 7225 typescript: "npm:^5.7.0" 7215 7226 vue: "npm:^3.5.0" 7216 7227 languageName: unknown ··· 9430 9441 languageName: node 9431 9442 linkType: hard 9432 9443 9444 + "resolve-pkg-maps@npm:^1.0.0": 9445 + version: 1.0.0 9446 + resolution: "resolve-pkg-maps@npm:1.0.0" 9447 + checksum: 10c0/fb8f7bbe2ca281a73b7ef423a1cbc786fb244bd7a95cbe5c3fba25b27d327150beca8ba02f622baea65919a57e061eb5005204daa5f93ed590d9b77463a567ab 9448 + languageName: node 9449 + linkType: hard 9450 + 9433 9451 "resolve@npm:^1.1.7, resolve@npm:^1.22.1, resolve@npm:^1.22.8": 9434 9452 version: 1.22.11 9435 9453 resolution: "resolve@npm:1.22.11" ··· 10386 10404 version: 1.0.6 10387 10405 resolution: "tsscmp@npm:1.0.6" 10388 10406 checksum: 10c0/2f79a9455e7e3e8071995f98cdf3487ccfc91b760bec21a9abb4d90519557eafaa37246e87c92fa8bf3fef8fd30cfd0cc3c4212bb929baa9fb62494bfa4d24b2 10407 + languageName: node 10408 + linkType: hard 10409 + 10410 + "tsx@npm:^4.19.0": 10411 + version: 4.21.0 10412 + resolution: "tsx@npm:4.21.0" 10413 + dependencies: 10414 + esbuild: "npm:~0.27.0" 10415 + fsevents: "npm:~2.3.3" 10416 + get-tsconfig: "npm:^4.7.5" 10417 + dependenciesMeta: 10418 + fsevents: 10419 + optional: true 10420 + bin: 10421 + tsx: dist/cli.mjs 10422 + checksum: 10c0/f5072923cd8459a1f9a26df87823a2ab5754641739d69df2a20b415f61814322b751fa6be85db7c6ec73cf68ba8fac2fd1cfc76bdb0aa86ded984d84d5d2126b 10389 10423 languageName: node 10390 10424 linkType: hard 10391 10425