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:
@@ -18,6 +18,11 @@ HYSTERIA_VERSION_OVERRIDE="${HYSTERIA_VERSION_OVERRIDE:-}"
|
||||
HYSTERIA_WRITE_LOCK="${HYSTERIA_WRITE_LOCK:-false}"
|
||||
# Compatibility gate обязателен для production-сборки.
|
||||
HYSTERIA_COMPAT_GATE="${HYSTERIA_COMPAT_GATE:-true}"
|
||||
# Сверка скачанного бинарника с upstream hashes.txt. Отключается только
|
||||
# break-glass: без неё SHA-256 считается от уже скачанного файла и является
|
||||
# trust-on-first-use, а не проверкой происхождения артефакта.
|
||||
HYSTERIA_VERIFY_UPSTREAM_HASHES="${HYSTERIA_VERIFY_UPSTREAM_HASHES:-true}"
|
||||
HYSTERIA_LINUX_AMD64_ASSET="hysteria-linux-amd64"
|
||||
|
||||
HYSTERIA_GATE_PORT="${HYSTERIA_GATE_PORT:-34443}"
|
||||
HYSTERIA_GATE_STATS_PORT="${HYSTERIA_GATE_STATS_PORT:-34712}"
|
||||
@@ -55,8 +60,12 @@ load_pinned_hysteria_lock() {
|
||||
|
||||
HYSTERIA_RESOLUTION="pinned"
|
||||
HYSTERIA_RESOLVED_AT="${HYSTERIA_RESOLVED_AT:-unknown}"
|
||||
# Сумма в lock-файле уже была проверена против upstream hashes.txt в тот
|
||||
# момент, когда lock записывался.
|
||||
HYSTERIA_SHA_SOURCE="hy2xs-lock"
|
||||
|
||||
export HYSTERIA_VERSION HYSTERIA_ARTIFACT_URL HYSTERIA_ARTIFACT_SHA256 HYSTERIA_RESOLUTION HYSTERIA_RESOLVED_AT
|
||||
export HYSTERIA_VERSION HYSTERIA_ARTIFACT_URL HYSTERIA_ARTIFACT_SHA256
|
||||
export HYSTERIA_RESOLUTION HYSTERIA_RESOLVED_AT HYSTERIA_SHA_SOURCE
|
||||
}
|
||||
|
||||
resolve_hysteria_from_upstream() {
|
||||
@@ -77,6 +86,7 @@ resolve_hysteria_from_upstream() {
|
||||
HYSTERIA_VERSION=""
|
||||
HYSTERIA_TAG=""
|
||||
HYSTERIA_ARTIFACT_URL=""
|
||||
HYSTERIA_HASHES_URL=""
|
||||
HYSTERIA_PUBLISHED_AT=""
|
||||
HYSTERIA_RESOLUTION=""
|
||||
|
||||
@@ -88,6 +98,7 @@ resolve_hysteria_from_upstream() {
|
||||
HYSTERIA_VERSION) HYSTERIA_VERSION="$value" ;;
|
||||
HYSTERIA_TAG) HYSTERIA_TAG="$value" ;;
|
||||
HYSTERIA_ARTIFACT_URL) HYSTERIA_ARTIFACT_URL="$value" ;;
|
||||
HYSTERIA_HASHES_URL) HYSTERIA_HASHES_URL="$value" ;;
|
||||
HYSTERIA_PUBLISHED_AT) HYSTERIA_PUBLISHED_AT="$value" ;;
|
||||
HYSTERIA_RESOLUTION) HYSTERIA_RESOLUTION="$value" ;;
|
||||
*) fail "unexpected resolver output line: $line" ;;
|
||||
@@ -96,14 +107,50 @@ resolve_hysteria_from_upstream() {
|
||||
|
||||
[ -n "$HYSTERIA_VERSION" ] || fail "resolver did not return HYSTERIA_VERSION"
|
||||
[ -n "$HYSTERIA_ARTIFACT_URL" ] || fail "resolver did not return HYSTERIA_ARTIFACT_URL"
|
||||
[ -n "$HYSTERIA_HASHES_URL" ] || fail "resolver did not return HYSTERIA_HASHES_URL"
|
||||
validate_hysteria_version_format "$HYSTERIA_VERSION"
|
||||
|
||||
HYSTERIA_RESOLVED_AT="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
||||
|
||||
log_info "Resolved Hysteria $HYSTERIA_VERSION (tag ${HYSTERIA_TAG:-unknown}, published ${HYSTERIA_PUBLISHED_AT:-unknown})"
|
||||
|
||||
export HYSTERIA_VERSION HYSTERIA_TAG HYSTERIA_ARTIFACT_URL HYSTERIA_PUBLISHED_AT
|
||||
export HYSTERIA_RESOLUTION HYSTERIA_RESOLVED_AT
|
||||
export HYSTERIA_VERSION HYSTERIA_TAG HYSTERIA_ARTIFACT_URL HYSTERIA_HASHES_URL
|
||||
export HYSTERIA_PUBLISHED_AT HYSTERIA_RESOLUTION HYSTERIA_RESOLVED_AT
|
||||
}
|
||||
|
||||
# Ожидаемый digest берётся из upstream-ассета hashes.txt ДО того, как он
|
||||
# попадёт в HY2XS lock. Иначе замороженная сумма подтверждает только то, что
|
||||
# файл не изменился после скачивания.
|
||||
resolve_expected_sha_from_upstream() {
|
||||
[ "$HYSTERIA_VERIFY_UPSTREAM_HASHES" = "true" ] || {
|
||||
log_info "WARNING: upstream hashes.txt verification is disabled (HYSTERIA_VERIFY_UPSTREAM_HASHES=$HYSTERIA_VERIFY_UPSTREAM_HASHES)"
|
||||
return 0
|
||||
}
|
||||
|
||||
[ -n "${HYSTERIA_HASHES_URL:-}" ] || fail "upstream hashes.txt URL is unknown"
|
||||
|
||||
mkdir -p "$HYSTERIA_WORK_DIR"
|
||||
local hashes_file="$HYSTERIA_WORK_DIR/hashes.txt"
|
||||
|
||||
log_info "Downloading upstream checksums: $HYSTERIA_HASHES_URL"
|
||||
rm -f "$hashes_file"
|
||||
curl --proto '=https' --tlsv1.2 --fail --silent --show-error --location \
|
||||
"$HYSTERIA_HASHES_URL" -o "$hashes_file" \
|
||||
|| fail "failed to download upstream hashes.txt: $HYSTERIA_HASHES_URL"
|
||||
[ -s "$hashes_file" ] || fail "downloaded upstream hashes.txt is empty"
|
||||
|
||||
local upstream_sha
|
||||
upstream_sha="$(cd orchestrator && "$BUN_BIN" run tools/parse-upstream-hashes.ts \
|
||||
--file "../$hashes_file" --asset "$HYSTERIA_LINUX_AMD64_ASSET")" \
|
||||
|| fail "could not read the expected SHA-256 from upstream hashes.txt"
|
||||
|
||||
validate_hysteria_sha256_format "$upstream_sha"
|
||||
|
||||
HYSTERIA_ARTIFACT_SHA256="$upstream_sha"
|
||||
HYSTERIA_SHA_SOURCE="upstream-hashes"
|
||||
export HYSTERIA_ARTIFACT_SHA256 HYSTERIA_SHA_SOURCE
|
||||
|
||||
log_info "Expected Hysteria SHA-256 from upstream hashes.txt: $upstream_sha"
|
||||
}
|
||||
|
||||
download_hysteria_artifact() {
|
||||
@@ -123,13 +170,17 @@ download_hysteria_artifact() {
|
||||
if [ -n "${HYSTERIA_ARTIFACT_SHA256:-}" ]; then
|
||||
validate_hysteria_sha256_format "$HYSTERIA_ARTIFACT_SHA256"
|
||||
[ "$actual_sha" = "$HYSTERIA_ARTIFACT_SHA256" ] \
|
||||
|| fail "Hysteria artifact sha256 mismatch: expected $HYSTERIA_ARTIFACT_SHA256, got $actual_sha"
|
||||
|| fail "Hysteria artifact sha256 mismatch: expected $HYSTERIA_ARTIFACT_SHA256 (${HYSTERIA_SHA_SOURCE:-unknown source}), got $actual_sha"
|
||||
log_info "Hysteria artifact verified against ${HYSTERIA_SHA_SOURCE:-unknown source}: $actual_sha"
|
||||
else
|
||||
# Сюда попадаем только при выключенной сверке с upstream: сумма
|
||||
# фиксируется как локальный fingerprint пакета, без проверки происхождения.
|
||||
HYSTERIA_ARTIFACT_SHA256="$actual_sha"
|
||||
log_info "Computed Hysteria artifact sha256: $HYSTERIA_ARTIFACT_SHA256"
|
||||
HYSTERIA_SHA_SOURCE="local-download"
|
||||
log_info "Computed Hysteria artifact sha256 without upstream verification: $HYSTERIA_ARTIFACT_SHA256"
|
||||
fi
|
||||
|
||||
export HYSTERIA_ARTIFACT_SHA256 HYSTERIA_BINARY
|
||||
export HYSTERIA_ARTIFACT_SHA256 HYSTERIA_BINARY HYSTERIA_SHA_SOURCE
|
||||
|
||||
chmod 0755 "$HYSTERIA_BINARY"
|
||||
|
||||
@@ -255,6 +306,7 @@ write_hysteria_lock_file() {
|
||||
HYSTERIA_VERSION=$HYSTERIA_VERSION
|
||||
HYSTERIA_ARTIFACT_URL=$HYSTERIA_ARTIFACT_URL
|
||||
HYSTERIA_ARTIFACT_SHA256=$HYSTERIA_ARTIFACT_SHA256
|
||||
HYSTERIA_SHA_SOURCE=${HYSTERIA_SHA_SOURCE:-unknown}
|
||||
HYSTERIA_RESOLUTION=$HYSTERIA_RESOLUTION
|
||||
HYSTERIA_RESOLVED_AT=$HYSTERIA_RESOLVED_AT
|
||||
EOF
|
||||
@@ -264,8 +316,11 @@ resolve_and_verify_hysteria() {
|
||||
case "$HYSTERIA_CHANNEL" in
|
||||
stable)
|
||||
resolve_hysteria_from_upstream
|
||||
# sha256 считается локально от скачанного артефакта
|
||||
# Ожидаемая сумма приходит из upstream hashes.txt, а не из локального
|
||||
# файла: проверяется происхождение артефакта, а не только целостность.
|
||||
HYSTERIA_ARTIFACT_SHA256=""
|
||||
HYSTERIA_SHA_SOURCE=""
|
||||
resolve_expected_sha_from_upstream
|
||||
;;
|
||||
pinned)
|
||||
log_info "Using pinned Hysteria lock file (offline): $HYSTERIA_LOCK_FILE"
|
||||
|
||||
Reference in New Issue
Block a user