-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathverify.ts
More file actions
112 lines (100 loc) · 3.78 KB
/
Copy pathverify.ts
File metadata and controls
112 lines (100 loc) · 3.78 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
/**
* End-to-end install verification.
*
* Given a package manifest, a signed ref-update certificate, and the bytes
* of the tarball that was downloaded for that version, the {@link verifyInstall}
* function returns a deterministic, stepwise report:
*
* 1. ref-update signatures meet the manifest's n-of-m threshold
* 2. every signer's DID is in the manifest's maintainer set
* 3. signatures collectively cover the version bump in scope (UCAN, optional)
* 4. ref-update.payload.cid matches the sha2-256 CID of the tarball bytes
* 5. caller-supplied expected CID (e.g. from lockfile) matches as well
*
* Any failure short-circuits the rest; the caller gets back the list of steps
* executed so far so failures can be reported with context.
*/
import { bytesToCidString, cidEqual } from "./cid.js";
import { countValidSignatures, type SignedRefUpdate } from "./ref.js";
import { type Manifest, validateManifest } from "./manifest.js";
export type VerifyStep = {
name: string;
ok: boolean;
detail?: string;
};
export type VerifyResult = {
ok: boolean;
steps: VerifyStep[];
};
export type VerifyInput = {
manifest: Manifest;
refUpdate: SignedRefUpdate;
tarball: Uint8Array;
/** CID the caller expects (e.g. from a lockfile entry). */
expectedCid: string;
};
export async function verifyInstall(input: VerifyInput): Promise<VerifyResult> {
const steps: VerifyStep[] = [];
// 0. Manifest is well-formed.
try {
validateManifest(input.manifest);
steps.push({ name: "manifest-valid", ok: true });
} catch (e) {
steps.push({ name: "manifest-valid", ok: false, detail: (e as Error).message });
return { ok: false, steps };
}
// 1. Signatures meet threshold.
const validSigs = countValidSignatures(input.refUpdate);
const sigsOk = validSigs >= input.manifest.threshold;
steps.push({
name: "signature-threshold",
ok: sigsOk,
detail: `${validSigs} / ${input.manifest.threshold}`
});
if (!sigsOk) return { ok: false, steps };
// 2. Every signer is in the manifest's maintainer set.
const known = new Set(input.manifest.maintainers.map((m) => m.did));
const unknown = input.refUpdate.signatures
.map((s) => s.did)
.filter((d) => !known.has(d));
if (unknown.length > 0) {
steps.push({ name: "signers-known", ok: false, detail: `unknown: ${unknown.join(", ")}` });
return { ok: false, steps };
}
steps.push({ name: "signers-known", ok: true });
// 3. Payload package name must match manifest name.
if (input.refUpdate.payload.name !== input.manifest.name) {
steps.push({
name: "name-match",
ok: false,
detail: `manifest=${input.manifest.name}, ref=${input.refUpdate.payload.name}`
});
return { ok: false, steps };
}
steps.push({ name: "name-match", ok: true });
// 4. CID of the tarball matches refUpdate.payload.cid.
const computedCid = await bytesToCidString(input.tarball);
const cidMatchesPayload = cidEqual(computedCid, input.refUpdate.payload.cid);
steps.push({
name: "tarball-cid",
ok: cidMatchesPayload,
detail: computedCid
});
if (!cidMatchesPayload) return { ok: false, steps };
// 5. Caller's expected CID matches as well (lockfile pin).
const lockMatch = cidEqual(input.expectedCid, input.refUpdate.payload.cid);
steps.push({ name: "expected-cid", ok: lockMatch });
if (!lockMatch) return { ok: false, steps };
return { ok: true, steps };
}
/** Render a verification report as a human-readable string for the CLI. */
export function formatReport(r: VerifyResult): string {
const lines: string[] = [];
for (const s of r.steps) {
const mark = s.ok ? "✓" : "✗";
lines.push(` ${mark} ${s.name.padEnd(22)} ${s.detail ?? ""}`.trimEnd());
}
lines.push("");
lines.push(r.ok ? " result · verified" : " result · refused");
return lines.join("\n");
}