@sanning/proof 0.9.0 → 0.9.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/verifier.js CHANGED
@@ -93,11 +93,27 @@ function resolveToJSON(node, max) {
93
93
  // bound exists to prevent, reached through the bound (proof#71 review). Copying
94
94
  // what was measured is what makes check and use the same value. It also settles
95
95
  // a getter, which is read once here and never re-read.
96
+ //
97
+ // Every object copy has a NULL prototype, and that is load-bearing too. RFC
98
+ // 8785 has no reserved member names, and `JSON.parse` keeps `__proto__` as an
99
+ // ordinary own key. On a plain `{}`, `copy["__proto__"] = x` runs the setter
100
+ // inherited from `Object.prototype`: it replaces the copy's prototype and
101
+ // creates no key, so the member and everything under it vanished from the
102
+ // canonical bytes. Content there was covered by no hash and no signature, and
103
+ // this kernel disagreed with the Python and Go kernels on the same bytes
104
+ // (0.9.0; the same bug class as proof#31 and proof#34). With no prototype
105
+ // there is no setter, so every key, `__proto__` included, lands as an own data
106
+ // property. `canonicalize` reads the copy with `Object.keys` and `object[key]`
107
+ // and tests `toJSON instanceof Function`, none of which a prototype affects
108
+ // for JSON data, so no other input's output changes.
109
+ function emptyObject() {
110
+ return Object.create(null);
111
+ }
96
112
  function boundedInput(value, max) {
97
113
  const root = resolveToJSON(value, max);
98
114
  if (root === OVER_DEEP || root === null || typeof root !== "object")
99
115
  return root;
100
- const copy = Array.isArray(root) ? [] : {};
116
+ const copy = Array.isArray(root) ? [] : emptyObject();
101
117
  const stack = [[root, copy, 1]];
102
118
  while (stack.length > 0) {
103
119
  const [src, dst, depth] = stack.pop();
@@ -113,7 +129,7 @@ function boundedInput(value, max) {
113
129
  }
114
130
  if (depth + 1 > max)
115
131
  return OVER_DEEP; // a container nested past the bound
116
- const childCopy = Array.isArray(child) ? [] : {};
132
+ const childCopy = Array.isArray(child) ? [] : emptyObject();
117
133
  dst[key] = childCopy;
118
134
  stack.push([child, childCopy, depth + 1]);
119
135
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sanning/proof",
3
- "version": "0.9.0",
3
+ "version": "0.9.1",
4
4
  "description": "TypeScript kernel of the Sanning verification stack: RFC 8785 (JCS) canonicalization, SHA-256, and Ed25519 verification for Verifiable Event Envelopes. Full-family verifier (inline + external commitment); accepts sanning.agent/v1, sanning.mlflow/v1, sanning.events/v1. Conformance-gated byte-for-byte against the test-vectors corpus.",
5
5
  "license": "MIT",
6
6
  "type": "module",
package/src/verifier.ts CHANGED
@@ -103,10 +103,27 @@ function resolveToJSON(node: unknown, max: number): unknown {
103
103
  // bound exists to prevent, reached through the bound (proof#71 review). Copying
104
104
  // what was measured is what makes check and use the same value. It also settles
105
105
  // a getter, which is read once here and never re-read.
106
+ //
107
+ // Every object copy has a NULL prototype, and that is load-bearing too. RFC
108
+ // 8785 has no reserved member names, and `JSON.parse` keeps `__proto__` as an
109
+ // ordinary own key. On a plain `{}`, `copy["__proto__"] = x` runs the setter
110
+ // inherited from `Object.prototype`: it replaces the copy's prototype and
111
+ // creates no key, so the member and everything under it vanished from the
112
+ // canonical bytes. Content there was covered by no hash and no signature, and
113
+ // this kernel disagreed with the Python and Go kernels on the same bytes
114
+ // (0.9.0; the same bug class as proof#31 and proof#34). With no prototype
115
+ // there is no setter, so every key, `__proto__` included, lands as an own data
116
+ // property. `canonicalize` reads the copy with `Object.keys` and `object[key]`
117
+ // and tests `toJSON instanceof Function`, none of which a prototype affects
118
+ // for JSON data, so no other input's output changes.
119
+ function emptyObject(): Record<string, unknown> {
120
+ return Object.create(null) as Record<string, unknown>;
121
+ }
122
+
106
123
  function boundedInput(value: unknown, max: number): unknown {
107
124
  const root = resolveToJSON(value, max);
108
125
  if (root === OVER_DEEP || root === null || typeof root !== "object") return root;
109
- const copy: Record<string, unknown> | unknown[] = Array.isArray(root) ? [] : {};
126
+ const copy: Record<string, unknown> | unknown[] = Array.isArray(root) ? [] : emptyObject();
110
127
  const stack: Array<[object, Record<string, unknown> | unknown[], number]> = [[root, copy, 1]];
111
128
  while (stack.length > 0) {
112
129
  const [src, dst, depth] = stack.pop()!;
@@ -120,7 +137,7 @@ function boundedInput(value: unknown, max: number): unknown {
120
137
  continue;
121
138
  }
122
139
  if (depth + 1 > max) return OVER_DEEP; // a container nested past the bound
123
- const childCopy: Record<string, unknown> | unknown[] = Array.isArray(child) ? [] : {};
140
+ const childCopy: Record<string, unknown> | unknown[] = Array.isArray(child) ? [] : emptyObject();
124
141
  (dst as Record<string, unknown>)[key] = childCopy;
125
142
  stack.push([child, childCopy, depth + 1]);
126
143
  }