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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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.
|
|
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": [
|