zapret-kvn/zapret_update/sign.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

133 lines
5.5 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.

"""Подпись списка выпуска на машине сборки.
Команды::
python -m zapret_update.sign keygen --key-id gui-2026a --out ключ.key
python -m zapret_update.sign make --key ключ.key --product zapret-gui \\
--channel dev --version 21.1.7.153 --tree dist/app \\
--installer Zapret2Setup.exe --exclude user --out папка_выпуска
``make`` пишет ``update-manifest.json`` и ``update-manifest.json.sig``.
Файл ключа — одна строка ``ed25519 <имя ключа> <32 байта в base64>``; его не
кладут в git и не показывают в журналах. ``keygen`` печатает открытую часть,
её вшивают в программу.
Подпись делает своя реализация Ed25519 (``ed25519``), сторонних библиотек
сборке не нужно.
"""
from __future__ import annotations
import argparse
import base64
import os
import sys
from pathlib import Path
from . import manifest as manifest_module
from .ed25519 import public_key_from_seed, sign_ed25519
def read_key(path: Path) -> tuple[str, bytes]:
"""``(имя ключа, 32 байта)`` из файла ключа."""
parts = Path(path).read_text(encoding="ascii").split()
if len(parts) != 3 or parts[0] != "ed25519":
raise ValueError(f"{path}: не файл ключа подписи")
seed = base64.b64decode(parts[2], validate=True)
if len(seed) != 32:
raise ValueError(f"{path}: ключ подписи повреждён")
return parts[1], seed
def generate_key(key_id: str, path: Path) -> str:
"""Создаёт ключ и возвращает открытую часть в base64. Существующий файл не перезаписывается."""
target = Path(path)
seed = os.urandom(32)
descriptor = os.open(target, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600)
with os.fdopen(descriptor, "w", encoding="ascii") as handle:
handle.write(f"ed25519 {key_id} {base64.b64encode(seed).decode('ascii')}\n")
return base64.b64encode(public_key_from_seed(seed)).decode("ascii")
def public_key_of(path: Path) -> str:
_key_id, seed = read_key(path)
return base64.b64encode(public_key_from_seed(seed)).decode("ascii")
def sign_bytes(raw: bytes, key_path: Path) -> tuple[str, str]:
"""``(имя ключа, подпись base64)`` над точными байтами."""
key_id, seed = read_key(key_path)
return key_id, base64.b64encode(sign_ed25519(seed, bytes(raw))).decode("ascii")
def make(
*,
key_path: Path,
product: str,
channel: str,
version: str,
out_dir: Path,
tree: Path | None = None,
installer: Path | None = None,
exclude: tuple[str, ...] = (),
) -> tuple[Path, Path]:
"""Собирает, подписывает и сверяет список выпуска; возвращает пути двух файлов."""
key_id, seed = read_key(key_path)
payload = manifest_module.build(
product=product, channel=channel, version=version, key=key_id, tree=tree, installer=installer, exclude=exclude
)
raw = manifest_module.dumps(payload)
_same_key_id, signature = sign_bytes(raw, key_path)
# Подпись сверяется той же проверкой, что стоит в программах.
public = public_key_from_seed(seed)
manifest_module.load_signed(raw, signature, trusted={key_id: public}, product=product, channel=channel)
out = Path(out_dir)
out.mkdir(parents=True, exist_ok=True)
manifest_path = out / manifest_module.MANIFEST_NAME
signature_path = out / manifest_module.SIGNATURE_NAME
manifest_path.write_bytes(raw)
signature_path.write_text(signature + "\n", encoding="ascii")
return manifest_path, signature_path
def main(argv: list[str] | None = None) -> int:
parser = argparse.ArgumentParser(description="Подпись списка выпуска Zapret.")
commands = parser.add_subparsers(dest="command", required=True)
keygen = commands.add_parser("keygen", help="создать ключ подписи")
keygen.add_argument("--key-id", required=True)
keygen.add_argument("--out", type=Path, required=True)
public = commands.add_parser("public", help="открытая часть ключа")
public.add_argument("--key", type=Path, required=True)
build = commands.add_parser("make", help="собрать и подписать список выпуска")
build.add_argument("--key", type=Path, required=True)
build.add_argument("--product", required=True)
build.add_argument("--channel", required=True)
build.add_argument("--version", required=True)
build.add_argument("--tree", type=Path)
build.add_argument("--installer", type=Path)
build.add_argument("--exclude", action="append", default=[])
build.add_argument("--out", type=Path, required=True)
args = parser.parse_args(argv)
if args.command == "keygen":
print(generate_key(args.key_id, args.out))
elif args.command == "public":
print(public_key_of(args.key))
else:
paths = make(
key_path=args.key,
product=args.product,
channel=args.channel,
version=args.version,
tree=args.tree,
installer=args.installer,
exclude=tuple(args.exclude),
out_dir=args.out,
)
print("\n".join(str(path) for path in paths))
return 0
if __name__ == "__main__":
sys.exit(main())