build: сверка артефакта Hysteria с upstream hashes.txt

SHA-256 считался локально от уже скачанного файла. Это защищает target
от последующей подмены, но не доказывает, что builder скачал именно
ожидаемый upstream artifact: сумма фиксирует то, что пришло, каким бы
оно ни было. То есть trust-on-first-use, а не проверка происхождения.

Upstream публикует контрольные суммы релиза ассетом hashes.txt:

  6493dfff...f94  build/hysteria-linux-amd64
  f24f63be...189  build/hysteria-linux-amd64-avx

Теперь резолвер отдаёт и URL этого ассета, сборка скачивает его,
берёт оттуда ожидаемую сумму и сверяет с ней бинарник - и только после
этого записывает SHA-256 в HY2XS lock и metadata.

Сопоставление идёт по базовому имени и строго на равенство: build/ -
часть пути, а hysteria-linux-amd64-avx - другой артефакт, который не
должен совпасть по префиксу. Разбор вынесен в parseUpstreamHashes и
покрыт тестами, включая форму sha256:<hex>, верхний регистр,
противоречивые и отсутствующие записи.

Релиз без hashes.txt для production-сборки непригоден и отклоняется.
Источник ожидаемой суммы фиксируется в metadata как hysteria_sha_source.
This commit is contained in:
2026-08-27 12:15:38 +05:00
parent 9da61c9482
commit 10d2c48cf0
7 changed files with 350 additions and 18 deletions
@@ -0,0 +1,54 @@
/**
* Build-time CLI: достаёт ожидаемый SHA-256 артефакта из upstream hashes.txt.
*
* Вынесено в отдельную команду, чтобы разбор формата жил в одном месте и был
* покрыт юнит-тестами, а не повторялся awk-выражением в bash.
*
* bun run tools/parse-upstream-hashes.ts --file hashes.txt --asset hysteria-linux-amd64
*/
import { parseUpstreamHashes } from "../src/build/hysteriaRelease";
function fail(message: string): never {
console.error(`[hy2xs-build] ERROR: ${message}`);
process.exit(1);
}
function parseArgs(argv: string[]): { file: string; asset: string } {
let file = "";
let asset = "";
for (let i = 0; i < argv.length; i += 1) {
const arg = argv[i];
const value = argv[i + 1];
if (arg === "--file") {
file = value ?? "";
i += 1;
continue;
}
if (arg === "--asset") {
asset = value ?? "";
i += 1;
continue;
}
fail(`unknown argument: ${arg}`);
}
if (!file) {
fail("missing --file");
}
if (!asset) {
fail("missing --asset");
}
return { file, asset };
}
const { file, asset } = parseArgs(Bun.argv.slice(2));
try {
const text = await Bun.file(file).text();
process.stdout.write(`${parseUpstreamHashes(text, asset)}\n`);
} catch (error) {
fail(error instanceof Error ? error.message : String(error));
}
+1
View File
@@ -102,6 +102,7 @@ function emit(resolved: ResolvedHysteriaRelease, resolution: string): void {
console.log(`HYSTERIA_VERSION=${resolved.version}`);
console.log(`HYSTERIA_TAG=${resolved.tag}`);
console.log(`HYSTERIA_ARTIFACT_URL=${resolved.artifactUrl}`);
console.log(`HYSTERIA_HASHES_URL=${resolved.hashesUrl}`);
console.log(`HYSTERIA_PUBLISHED_AT=${resolved.publishedAt}`);
console.log(`HYSTERIA_RESOLUTION=${resolution}`);
}