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:
@@ -8,6 +8,13 @@
|
||||
export const HYSTERIA_UPSTREAM_REPO = "HyNetworks/hysteria";
|
||||
export const HYSTERIA_LINUX_AMD64_ASSET = "hysteria-linux-amd64";
|
||||
|
||||
/**
|
||||
* Upstream публикует контрольные суммы релиза отдельным ассетом. Он позволяет
|
||||
* проверить ПРОИСХОЖДЕНИЕ артефакта, а не только его неизменность: без него
|
||||
* SHA-256 считается от уже скачанного файла, то есть является trust-on-first-use.
|
||||
*/
|
||||
export const HYSTERIA_HASHES_ASSET = "hashes.txt";
|
||||
|
||||
/** Upstream публикует несколько семейств тегов; серверный бинарник живёт в app/vX.Y.Z. */
|
||||
export const HYSTERIA_APP_TAG_PATTERN = /^app\/v(\d+)\.(\d+)\.(\d+)$/;
|
||||
export const HYSTERIA_VERSION_PATTERN = /^v(\d+)\.(\d+)\.(\d+)$/;
|
||||
@@ -32,6 +39,8 @@ export type ResolvedHysteriaRelease = {
|
||||
tag: string;
|
||||
/** Immutable release asset URL ровно в том виде, как его отдал GitHub API. */
|
||||
artifactUrl: string;
|
||||
/** URL ассета с контрольными суммами релиза. */
|
||||
hashesUrl: string;
|
||||
publishedAt: string;
|
||||
};
|
||||
|
||||
@@ -68,17 +77,15 @@ function isStable(release: GithubRelease): boolean {
|
||||
return release.draft !== true && release.prerelease !== true;
|
||||
}
|
||||
|
||||
export function selectLinuxAmd64Asset(release: GithubRelease): GithubAsset {
|
||||
const matches = (release.assets ?? []).filter((asset) => asset.name === HYSTERIA_LINUX_AMD64_ASSET);
|
||||
export function selectAssetByName(release: GithubRelease, assetName: string): GithubAsset {
|
||||
const matches = (release.assets ?? []).filter((asset) => asset.name === assetName);
|
||||
|
||||
if (matches.length === 0) {
|
||||
throw new Error(
|
||||
`upstream release ${release.tag_name} has no ${HYSTERIA_LINUX_AMD64_ASSET} asset`
|
||||
);
|
||||
throw new Error(`upstream release ${release.tag_name} has no ${assetName} asset`);
|
||||
}
|
||||
if (matches.length > 1) {
|
||||
throw new Error(
|
||||
`upstream release ${release.tag_name} has ${matches.length} ambiguous ${HYSTERIA_LINUX_AMD64_ASSET} assets`
|
||||
`upstream release ${release.tag_name} has ${matches.length} ambiguous ${assetName} assets`
|
||||
);
|
||||
}
|
||||
|
||||
@@ -93,11 +100,79 @@ export function selectLinuxAmd64Asset(release: GithubRelease): GithubAsset {
|
||||
return { name: matches[0].name, browser_download_url: url };
|
||||
}
|
||||
|
||||
export function selectLinuxAmd64Asset(release: GithubRelease): GithubAsset {
|
||||
return selectAssetByName(release, HYSTERIA_LINUX_AMD64_ASSET);
|
||||
}
|
||||
|
||||
export function selectHashesAsset(release: GithubRelease): GithubAsset {
|
||||
return selectAssetByName(release, HYSTERIA_HASHES_ASSET);
|
||||
}
|
||||
|
||||
/**
|
||||
* Разбирает upstream hashes.txt.
|
||||
*
|
||||
* Фактический формат релиза app/v2.12.2:
|
||||
*
|
||||
* 6493dfff…f94 build/hysteria-linux-amd64
|
||||
* f24f63be…189 build/hysteria-linux-amd64-avx
|
||||
*
|
||||
* Сопоставление идёт по базовому имени и строго на равенство: префикс
|
||||
* `build/` — часть пути, а `hysteria-linux-amd64-avx` — другой артефакт,
|
||||
* который не должен совпасть по prefix-match.
|
||||
*
|
||||
* Поддерживается и форма `sha256:<hex>`: если upstream когда-нибудь сменит
|
||||
* стиль, сборка не должна падать на косметике.
|
||||
*/
|
||||
export function parseUpstreamHashes(text: string, assetName: string): string {
|
||||
const candidates: string[] = [];
|
||||
|
||||
for (const rawLine of text.split(/\r?\n/)) {
|
||||
const line = rawLine.trim();
|
||||
if (!line || line.startsWith("#")) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const columns = line.split(/\s+/).filter(Boolean);
|
||||
if (columns.length < 2) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const [first, ...rest] = columns;
|
||||
const digest = first.toLowerCase().replace(/^sha256:/, "");
|
||||
if (!/^[a-f0-9]{64}$/.test(digest)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const named = rest.some((column) => {
|
||||
const basename = column.replace(/^\*/, "").split("/").pop() ?? "";
|
||||
return basename === assetName;
|
||||
});
|
||||
|
||||
if (named) {
|
||||
candidates.push(digest);
|
||||
}
|
||||
}
|
||||
|
||||
if (candidates.length === 0) {
|
||||
throw new Error(`upstream hashes.txt has no SHA-256 entry for ${assetName}`);
|
||||
}
|
||||
|
||||
const unique = [...new Set(candidates)];
|
||||
if (unique.length > 1) {
|
||||
throw new Error(
|
||||
`upstream hashes.txt has conflicting SHA-256 entries for ${assetName}: ${unique.join(", ")}`
|
||||
);
|
||||
}
|
||||
|
||||
return unique[0];
|
||||
}
|
||||
|
||||
function toResolved(release: GithubRelease, version: string): ResolvedHysteriaRelease {
|
||||
return {
|
||||
version,
|
||||
tag: release.tag_name.trim(),
|
||||
artifactUrl: selectLinuxAmd64Asset(release).browser_download_url,
|
||||
hashesUrl: selectHashesAsset(release).browser_download_url,
|
||||
publishedAt: (release.published_at ?? "").trim()
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user