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:
@@ -1,10 +1,13 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import {
|
||||
HYSTERIA_HASHES_ASSET,
|
||||
HYSTERIA_LINUX_AMD64_ASSET,
|
||||
compareVersionParts,
|
||||
parseAppTag,
|
||||
parseUpstreamHashes,
|
||||
parseVersion,
|
||||
renderLockEnv,
|
||||
selectHashesAsset,
|
||||
selectLatestStableRelease,
|
||||
selectLinuxAmd64Asset,
|
||||
selectReleaseByVersion,
|
||||
@@ -12,8 +15,15 @@ import {
|
||||
} from "../src/build/hysteriaRelease";
|
||||
|
||||
/** Upstream отдаёт URL с неэкранированным слэшем в теге; берём его как есть. */
|
||||
function assetUrl(tag: string): string {
|
||||
return `https://github.com/HyNetworks/hysteria/releases/download/${tag}/${HYSTERIA_LINUX_AMD64_ASSET}`;
|
||||
function assetUrl(tag: string, name = HYSTERIA_LINUX_AMD64_ASSET): string {
|
||||
return `https://github.com/HyNetworks/hysteria/releases/download/${tag}/${name}`;
|
||||
}
|
||||
|
||||
function defaultAssets(tag: string) {
|
||||
return [
|
||||
{ name: HYSTERIA_LINUX_AMD64_ASSET, browser_download_url: assetUrl(tag) },
|
||||
{ name: HYSTERIA_HASHES_ASSET, browser_download_url: assetUrl(tag, HYSTERIA_HASHES_ASSET) }
|
||||
];
|
||||
}
|
||||
|
||||
function release(tag: string, overrides: Partial<GithubRelease> = {}): GithubRelease {
|
||||
@@ -22,7 +32,7 @@ function release(tag: string, overrides: Partial<GithubRelease> = {}): GithubRel
|
||||
draft: false,
|
||||
prerelease: false,
|
||||
published_at: "2026-08-23T00:59:00Z",
|
||||
assets: [{ name: HYSTERIA_LINUX_AMD64_ASSET, browser_download_url: assetUrl(tag) }],
|
||||
assets: defaultAssets(tag),
|
||||
...overrides
|
||||
};
|
||||
}
|
||||
@@ -98,11 +108,138 @@ describe("selectLatestStableRelease", () => {
|
||||
const encoded = "https://github.com/HyNetworks/hysteria/releases/download/app%2Fv2.12.2/hysteria-linux-amd64";
|
||||
const resolved = selectLatestStableRelease([
|
||||
release("app/v2.12.2", {
|
||||
assets: [{ name: HYSTERIA_LINUX_AMD64_ASSET, browser_download_url: encoded }]
|
||||
assets: [
|
||||
{ name: HYSTERIA_LINUX_AMD64_ASSET, browser_download_url: encoded },
|
||||
{
|
||||
name: HYSTERIA_HASHES_ASSET,
|
||||
browser_download_url: assetUrl("app/v2.12.2", HYSTERIA_HASHES_ASSET)
|
||||
}
|
||||
]
|
||||
})
|
||||
]);
|
||||
expect(resolved.artifactUrl).toBe(encoded);
|
||||
});
|
||||
|
||||
test("резолв отдаёт и url ассета с контрольными суммами", () => {
|
||||
const resolved = selectLatestStableRelease(UPSTREAM_FIXTURE);
|
||||
expect(resolved.hashesUrl).toBe(
|
||||
"https://github.com/HyNetworks/hysteria/releases/download/app/v2.12.2/hashes.txt"
|
||||
);
|
||||
});
|
||||
|
||||
// Без hashes.txt проверять происхождение артефакта нечем, поэтому такой
|
||||
// релиз для production-сборки непригоден.
|
||||
test("релиз без hashes.txt отклоняется", () => {
|
||||
const noHashes = release("app/v2.12.2", {
|
||||
assets: [{ name: HYSTERIA_LINUX_AMD64_ASSET, browser_download_url: assetUrl("app/v2.12.2") }]
|
||||
});
|
||||
expect(() => selectLatestStableRelease([noHashes])).toThrow(/has no hashes\.txt asset/);
|
||||
});
|
||||
});
|
||||
|
||||
describe("selectHashesAsset", () => {
|
||||
test("находит ассет контрольных сумм", () => {
|
||||
expect(selectHashesAsset(release("app/v2.12.2")).name).toBe(HYSTERIA_HASHES_ASSET);
|
||||
});
|
||||
|
||||
test("дублирующийся hashes.txt — ошибка", () => {
|
||||
const ambiguous = release("app/v2.12.2", {
|
||||
assets: [
|
||||
...defaultAssets("app/v2.12.2"),
|
||||
{ name: HYSTERIA_HASHES_ASSET, browser_download_url: "https://example.com/evil" }
|
||||
]
|
||||
});
|
||||
expect(() => selectHashesAsset(ambiguous)).toThrow(/ambiguous/);
|
||||
});
|
||||
});
|
||||
|
||||
describe("parseUpstreamHashes", () => {
|
||||
// Реальный формат релиза app/v2.12.2.
|
||||
const UPSTREAM_HASHES = [
|
||||
"6493dfffd55b5883f64c76c63880ecc32988f0c568c9ca9014907877b4d55f94 build/hysteria-linux-amd64",
|
||||
"f24f63bece90b91b4e47d392f0afa4d8fe6d38785cca0589c1d121fd53b8e189 build/hysteria-linux-amd64-avx",
|
||||
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa build/hysteria-linux-arm64"
|
||||
].join("\n");
|
||||
|
||||
test("достаёт сумму нужного артефакта", () => {
|
||||
expect(parseUpstreamHashes(UPSTREAM_HASHES, HYSTERIA_LINUX_AMD64_ASSET)).toBe(
|
||||
"6493dfffd55b5883f64c76c63880ecc32988f0c568c9ca9014907877b4d55f94"
|
||||
);
|
||||
});
|
||||
|
||||
// Ключевой момент: -avx не должен совпасть по префиксу с базовым артефактом.
|
||||
test("не путает hysteria-linux-amd64 с hysteria-linux-amd64-avx", () => {
|
||||
expect(parseUpstreamHashes(UPSTREAM_HASHES, HYSTERIA_LINUX_AMD64_ASSET)).not.toBe(
|
||||
"f24f63bece90b91b4e47d392f0afa4d8fe6d38785cca0589c1d121fd53b8e189"
|
||||
);
|
||||
expect(parseUpstreamHashes(UPSTREAM_HASHES, "hysteria-linux-amd64-avx")).toBe(
|
||||
"f24f63bece90b91b4e47d392f0afa4d8fe6d38785cca0589c1d121fd53b8e189"
|
||||
);
|
||||
});
|
||||
|
||||
test("работает без префикса каталога", () => {
|
||||
const flat = `6493dfffd55b5883f64c76c63880ecc32988f0c568c9ca9014907877b4d55f94 ${HYSTERIA_LINUX_AMD64_ASSET}`;
|
||||
expect(parseUpstreamHashes(flat, HYSTERIA_LINUX_AMD64_ASSET)).toBe(
|
||||
"6493dfffd55b5883f64c76c63880ecc32988f0c568c9ca9014907877b4d55f94"
|
||||
);
|
||||
});
|
||||
|
||||
test("понимает форму sha256:<hex> и бинарный маркер '*'", () => {
|
||||
const alternative = [
|
||||
`sha256:6493dfffd55b5883f64c76c63880ecc32988f0c568c9ca9014907877b4d55f94 *${HYSTERIA_LINUX_AMD64_ASSET}`
|
||||
].join("\n");
|
||||
expect(parseUpstreamHashes(alternative, HYSTERIA_LINUX_AMD64_ASSET)).toBe(
|
||||
"6493dfffd55b5883f64c76c63880ecc32988f0c568c9ca9014907877b4d55f94"
|
||||
);
|
||||
});
|
||||
|
||||
test("верхний регистр нормализуется", () => {
|
||||
const upper = `6493DFFFD55B5883F64C76C63880ECC32988F0C568C9CA9014907877B4D55F94 ${HYSTERIA_LINUX_AMD64_ASSET}`;
|
||||
expect(parseUpstreamHashes(upper, HYSTERIA_LINUX_AMD64_ASSET)).toBe(
|
||||
"6493dfffd55b5883f64c76c63880ecc32988f0c568c9ca9014907877b4d55f94"
|
||||
);
|
||||
});
|
||||
|
||||
test("комментарии и пустые строки игнорируются", () => {
|
||||
const noisy = [
|
||||
"# hysteria release checksums",
|
||||
"",
|
||||
`6493dfffd55b5883f64c76c63880ecc32988f0c568c9ca9014907877b4d55f94 build/${HYSTERIA_LINUX_AMD64_ASSET}`
|
||||
].join("\n");
|
||||
expect(parseUpstreamHashes(noisy, HYSTERIA_LINUX_AMD64_ASSET)).toBe(
|
||||
"6493dfffd55b5883f64c76c63880ecc32988f0c568c9ca9014907877b4d55f94"
|
||||
);
|
||||
});
|
||||
|
||||
test("отсутствие нужной строки — ошибка, а не тихий пропуск проверки", () => {
|
||||
expect(() => parseUpstreamHashes(UPSTREAM_HASHES, "hysteria-linux-riscv64")).toThrow(
|
||||
/no SHA-256 entry for hysteria-linux-riscv64/
|
||||
);
|
||||
expect(() => parseUpstreamHashes("", HYSTERIA_LINUX_AMD64_ASSET)).toThrow(/no SHA-256 entry/);
|
||||
});
|
||||
|
||||
test("противоречивые записи — ошибка, а не выбор первой", () => {
|
||||
const conflicting = [
|
||||
`6493dfffd55b5883f64c76c63880ecc32988f0c568c9ca9014907877b4d55f94 build/${HYSTERIA_LINUX_AMD64_ASSET}`,
|
||||
`bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb dist/${HYSTERIA_LINUX_AMD64_ASSET}`
|
||||
].join("\n");
|
||||
expect(() => parseUpstreamHashes(conflicting, HYSTERIA_LINUX_AMD64_ASSET)).toThrow(/conflicting/);
|
||||
});
|
||||
|
||||
test("повторяющаяся одинаковая запись ошибкой не считается", () => {
|
||||
const duplicated = [
|
||||
`6493dfffd55b5883f64c76c63880ecc32988f0c568c9ca9014907877b4d55f94 build/${HYSTERIA_LINUX_AMD64_ASSET}`,
|
||||
`6493dfffd55b5883f64c76c63880ecc32988f0c568c9ca9014907877b4d55f94 ${HYSTERIA_LINUX_AMD64_ASSET}`
|
||||
].join("\n");
|
||||
expect(parseUpstreamHashes(duplicated, HYSTERIA_LINUX_AMD64_ASSET)).toBe(
|
||||
"6493dfffd55b5883f64c76c63880ecc32988f0c568c9ca9014907877b4d55f94"
|
||||
);
|
||||
});
|
||||
|
||||
test("строка с мусором вместо digest игнорируется", () => {
|
||||
const garbage = `not-a-sha build/${HYSTERIA_LINUX_AMD64_ASSET}`;
|
||||
expect(() => parseUpstreamHashes(garbage, HYSTERIA_LINUX_AMD64_ASSET)).toThrow(/no SHA-256 entry/);
|
||||
});
|
||||
});
|
||||
|
||||
describe("selectLinuxAmd64Asset", () => {
|
||||
|
||||
@@ -2,11 +2,16 @@ import { describe, expect, test } from "bun:test";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import {
|
||||
HYSTERIA_HASHES_ASSET,
|
||||
HYSTERIA_LINUX_AMD64_ASSET,
|
||||
selectLatestStableRelease,
|
||||
type GithubRelease
|
||||
} from "../src/build/hysteriaRelease";
|
||||
|
||||
function assetUrl(tag: string, name: string): string {
|
||||
return `https://github.com/HyNetworks/hysteria/releases/download/${tag}/${name}`;
|
||||
}
|
||||
|
||||
function release(tag: string): GithubRelease {
|
||||
return {
|
||||
tag_name: tag,
|
||||
@@ -16,7 +21,11 @@ function release(tag: string): GithubRelease {
|
||||
assets: [
|
||||
{
|
||||
name: HYSTERIA_LINUX_AMD64_ASSET,
|
||||
browser_download_url: `https://github.com/HyNetworks/hysteria/releases/download/${tag}/${HYSTERIA_LINUX_AMD64_ASSET}`
|
||||
browser_download_url: assetUrl(tag, HYSTERIA_LINUX_AMD64_ASSET)
|
||||
},
|
||||
{
|
||||
name: HYSTERIA_HASHES_ASSET,
|
||||
browser_download_url: assetUrl(tag, HYSTERIA_HASHES_ASSET)
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user