fix: harden provider and promotion evidence

This commit is contained in:
DongHyeonka
2026-08-02 16:28:24 +09:00
parent 42ffb79997
commit 30ceac23c1
29 changed files with 3961 additions and 1076 deletions
+56
View File
@@ -36,6 +36,62 @@ const MAX_MEMBER_PATH_BYTES = 1_024;
const TAR_EXECUTABLE = "/usr/bin/tar";
const TAR_ENVIRONMENT = Object.freeze({ PATH: "/usr/bin:/bin", LC_ALL: "C", LANG: "C" });
export type CapturedCandidateArchive = Readonly<{
bytes: Buffer;
archiveSha256: string;
}>;
export async function captureCiCandidateArchive(input: Readonly<{
archivePath: string;
expectedSha256: string;
}>): Promise<CapturedCandidateArchive> {
if (!/^[a-f0-9]{64}$/u.test(input.expectedSha256)) {
throw new TypeError("expected candidate archive SHA-256 is invalid");
}
const absolute = path.resolve(input.archivePath);
const before = await lstat(absolute);
if (!before.isFile() || before.isSymbolicLink()) {
throw new TypeError("candidate archive must be a regular non-symlink file");
}
if (before.size <= 0 || before.size > MAX_ARCHIVE_BYTES) {
throw new RangeError(`candidate archive size is outside 1..${MAX_ARCHIVE_BYTES}`);
}
const handle = await open(absolute, constants.O_RDONLY | constants.O_NOFOLLOW);
let bytes: Buffer;
try {
assertSameIdentity(before, await handle.stat());
bytes = await readCapturedArchive(handle, before.size);
assertSameIdentity(before, await handle.stat());
} finally {
await handle.close();
}
const archiveSha256 = createHash("sha256").update(bytes).digest("hex");
if (archiveSha256 !== input.expectedSha256) {
throw new Error("candidate archive SHA-256 mismatch");
}
return Object.freeze({ bytes, archiveSha256 });
}
export async function withVerifiedCapturedCandidate<T>(input: Readonly<{
captured: CapturedCandidateArchive;
verify: (view: Readonly<{
extractionRoot: string;
manifest: ReleaseCandidateManifest;
}>) => Promise<T>;
}>): Promise<T> {
let result: T | undefined;
await verifyCapturedCiCandidateArchive(
input.captured.bytes,
input.captured.archiveSha256,
{
verifyExtracted: async (extractionRoot, manifest) => {
result = await input.verify({ extractionRoot, manifest });
},
},
);
return result as T;
}
export async function verifyCiCandidateArchive(
input: Readonly<{
archivePath: string;