zapret-kvn/xray_fluent/updates/signed_release.py
loop-uh 5d8c27cd47
Some checks failed
Windows project source guards / test (push) Has been cancelled
Обновление: архив ставится только по подписанному списку выпуска
Общее ядро zapret_update (байт-в-байт копия из Zapret 2 GUI, переносится
scripts/sync_update_core.py, тест сверяет сумму копии). Программа
проверяет update-manifest.json и подпись ключами kvn-2026a и запасным и
берёт сумму и размер архива только из списка. Сценарий выпуска
подписывает архив, сверяет подпись с ключами программы этого коммита и
выкладывает список вместе с остальными файлами выпуска.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 20:02:14 +03:00

65 lines
3.1 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""Подписанный список выпуска Zapret KVN: чем проверить архив обновления.
Сценарий выпуска кладёт в выпуск Forgejo ``update-manifest.json`` и подпись.
Программа скачивает их вместе с данными выпуска и проверяет подпись
открытыми ключами ниже (общее ядро ``zapret_update``, то же, что в Zapret 2
GUI). Контрольная сумма и размер архива берутся только из проверенного
списка: подменённый архив с подставленной суммой не пройдёт.
Ключи — рабочий и запасной; закрытые части у владельца, в git их нет.
"""
from __future__ import annotations
import base64
from collections.abc import Callable, Mapping
from zapret_update.manifest import MANIFEST_NAME, MAX_MANIFEST_BYTES, SIGNATURE_NAME, Manifest, ManifestError, load_signed
PRODUCT = "zapret-kvn"
CHANNEL = "stable"
TRUSTED_KEYS: dict[str, bytes] = {
"kvn-2026a": base64.b64decode("StnbabfKxv5ppg6qVBBMfJKY2j0LqG+IMtjRq3uYihE="),
"kvn-reserve-1": base64.b64decode("m+YPRcb0w92jeopD6BjPj6TvsAh97GdAwBytFcZOrIM="),
}
MAX_SIGNATURE_BYTES = 1024
def signed_asset_urls(assets: list[Mapping], is_trusted: Callable[[str], bool]) -> tuple[str, str]:
"""Ссылки на список и подпись среди файлов выпуска; пустые, если их нет."""
found = {}
for asset in assets:
name = str(asset.get("name") or "")
url = str(asset.get("browser_download_url") or "")
if name in (MANIFEST_NAME, SIGNATURE_NAME) and is_trusted(url):
found[name] = url
return found.get(MANIFEST_NAME, ""), found.get(SIGNATURE_NAME, "")
def verified_manifest(
*,
version: str,
current_version: str,
manifest_url: str,
signature_url: str,
fetch: Callable[[str, int], bytes],
) -> Manifest:
"""Проверенный список выпуска ``version``; иначе ``ManifestError``.
``fetch(url, предел_байт)`` — скачивание тем же путём, что данные выпуска.
"""
if not manifest_url or not signature_url:
raise ManifestError("в выпуске нет подписанного списка", "nosig")
raw = fetch(manifest_url, MAX_MANIFEST_BYTES)
signature = fetch(signature_url, MAX_SIGNATURE_BYTES)
manifest = load_signed(
raw, signature, trusted=TRUSTED_KEYS, product=PRODUCT, channel=CHANNEL, newer_than=current_version
)
if manifest.version != version:
raise ManifestError(f"выпуск {version}, а подписанный список — {manifest.version}", "sign")
if manifest.installer is None:
raise ManifestError("в подписанном списке выпуска нет архива", "format")
return manifest
__all__ = ["CHANNEL", "PRODUCT", "TRUSTED_KEYS", "signed_asset_urls", "verified_manifest"]