tewip-ai 0.69.10 → 0.70.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.
@@ -14,7 +14,9 @@ export function verifyAttestation(file, keyFile) {
14
14
  const bundle = JSON.parse(readFileSync(resolve(file), 'utf8'));
15
15
  const publicKey = keyFile ? readFileSync(resolve(keyFile), 'utf8') : undefined;
16
16
  const checked = check(bundle, publicKey);
17
- const ok = checked.contentMatches && checked.signature !== 'invalid';
17
+ // 'stripped' counts as a failure: the caller supplied a key, so it asked who made this, and a
18
+ // bundle with no signature cannot answer. See `check` in seal.ts for the attack that exposed it.
19
+ const ok = checked.contentMatches && checked.signature !== 'invalid' && checked.signature !== 'stripped';
18
20
  const report = [
19
21
  `# ${ok ? 'Checked' : 'Do not trust this bundle'}: ${file.split(/[\/]/).pop()}`,
20
22
  '',
@@ -20,18 +20,24 @@ import { createHash, createPrivateKey, createPublicKey, sign as cryptoSign, veri
20
20
  * different bundle.
21
21
  */
22
22
  export function canonical(bundle) {
23
- const ordered = (value) => {
23
+ // The seal is left out of its own digest, which it has to be - but only the **top-level** one.
24
+ // The filter used to apply at every depth, so any field named `seal` anywhere inside the bundle
25
+ // was invisible to the digest: content could be added under `claims[0].seal` without changing the
26
+ // digest or invalidating the signature over it. Nothing reads such a field today, which is why
27
+ // this is small rather than serious; the claim being made is that the digest covers the content,
28
+ // and a digest with a hole in it is not that claim.
29
+ const ordered = (value, top = false) => {
24
30
  if (Array.isArray(value))
25
- return value.map(ordered);
31
+ return value.map((v) => ordered(v));
26
32
  if (value && typeof value === 'object') {
27
33
  return Object.fromEntries(Object.entries(value)
28
- .filter(([k]) => k !== 'seal')
34
+ .filter(([k]) => !(top && k === 'seal'))
29
35
  .sort(([a], [b]) => a.localeCompare(b))
30
36
  .map(([k, v]) => [k, ordered(v)]));
31
37
  }
32
38
  return value;
33
39
  };
34
- return JSON.stringify(ordered(bundle));
40
+ return JSON.stringify(ordered(bundle, true));
35
41
  }
36
42
  export const digestOf = (bundle) => createHash('sha256').update(canonical(bundle)).digest('hex');
37
43
  /**
@@ -65,10 +71,27 @@ export function check(bundle, publicKeyPem) {
65
71
  return { contentMatches: false, signature: bundle.seal?.signature ? 'invalid' : 'absent', says: 'the content does not match the digest recorded inside it: it has been changed since it was written' };
66
72
  }
67
73
  if (!bundle.seal?.signature) {
74
+ // A key was given, so authorship is what was asked about - and there is nothing here to answer
75
+ // with. This used to return the same pass as an unsigned bundle checked without a key, so:
76
+ // change a fact, erase the "what was not checked" list, delete the signature, recompute the
77
+ // digest, and `attest --verify bundle.json --key release.pem` printed "Content matches its
78
+ // digest: yes" and **exited 0**. Any gate scripted on it passed. Removing a signature is the
79
+ // oldest attack on one, and "there is nothing to check" is a failure to establish what was
80
+ // asked, not a pass.
81
+ if (publicKeyPem?.trim()) {
82
+ return {
83
+ contentMatches: true,
84
+ signature: 'stripped',
85
+ says: 'a public key was given but this bundle carries no signature, so who produced it cannot be established. ' +
86
+ 'The content does match the digest recorded inside it, and that is worth nothing here: anyone who edits a bundle can recompute its digest, which is exactly what removing the signature would look like',
87
+ };
88
+ }
68
89
  return {
69
90
  contentMatches: true,
70
91
  signature: 'absent',
71
- says: 'the content matches its own digest, and nothing more: no signature was made, so this shows the bundle is undamaged, not that nobody edited it - an editor can recompute a digest',
92
+ // "no signature was made" asserted something about the bundle's history that cannot be known
93
+ // from the bundle: it may have been removed. What is observable is that it carries none.
94
+ says: 'the content matches its own digest, and nothing more: this bundle carries no signature, so this shows it is undamaged, not that nobody edited it - an editor can recompute a digest',
72
95
  };
73
96
  }
74
97
  if (!publicKeyPem?.trim()) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tewip-ai",
3
- "version": "0.69.10",
3
+ "version": "0.70.0",
4
4
  "mcpName": "io.github.hmamut39/tewip",
5
5
  "description": "Tewip finds why each failing test failed - real bug, flaky test, environment, test data or changed selector - for any stack, from the command line, CI, or your editor.",
6
6
  "keywords": [