Files
voculinux/packaging/void/xbps-fingerprint.py
T
xavierk 05ef78922d fix(void): build the xbps release from the tag, not an attached sdist
The signed Void release depended on a fork sdist attached to the release
before the tag existed, and the app template carried a PENDING checksum that
had to be filled by hand. Every release is now built from its tag alone:

- packaging/void/build-release.sh is the one release path. The Gitea workflow,
  the local dry run and the smoke gate all call it.
- The canonical reproducible sdist is built from the tag with
  scripts/build_reproducible_wheel.py (Python 3.11, pinned uv).
- render-voculinux.sh pins version, _tag and checksum in the container's build
  tree only, and places the sdist where xbps-src resolves its distfile.
  The committed template keeps a zero placeholder until the post-release
  checksum pull request.
- xbps version is __version__ with the hyphen removed (0.19.0-beta becomes
  0.19.0beta), which xbps sorts below the final release.
- Builds run without the key; signing runs with the key mounted read-only and
  no network; the install smoke runs in a clean image without the key.
- The workflow gates the tag and the key before any container starts, writes
  the key to a 0600 tmpfs file, and publishes only the staged set (sdist,
  packages, signatures, repodata, client key).
- smoke.sh runs the same pipeline with a throwaway key.
2026-11-06 20:06:35 +05:30

96 lines
3.2 KiB
Python
Executable File

#!/usr/bin/env python3
"""xbps repository-key helper for packaging/void/build-release.sh.
xbps-fingerprint.py fingerprint PEM
print the xbps fingerprint of an RSA public key (PEM): the MD5 of the
ssh-rsa blob of the key, the identifier libxbps calls xbps_pubkey2fp.
xbps-fingerprint.py plist PEM SIGNER OUT
write the client key plist xbps expects for that public key. Used only
for a throwaway smoke key; the release publishes the committed
repository-key.plist byte for byte.
Only the public key is ever read here. Run with -I: the interpreter must not
pick up modules from the directory a downloaded file sits in.
"""
from __future__ import annotations
import hashlib
import plistlib
import re
import struct
import subprocess
import sys
from pathlib import Path
def _rsa_numbers(pem: Path) -> tuple[int, int]:
"""Modulus and exponent of a PEM public key, read through openssl."""
text = subprocess.run(
["openssl", "rsa", "-pubin", "-in", str(pem), "-noout", "-text"],
check=True,
capture_output=True,
text=True,
).stdout
modulus_hex: list[str] = []
exponent = None
in_modulus = False
for line in text.splitlines():
stripped = line.strip()
if stripped.startswith("Modulus"):
in_modulus = True
continue
if stripped.startswith("Exponent"):
in_modulus = False
exponent = int(stripped.split()[1])
continue
if in_modulus and re.fullmatch(r"[0-9a-f:]+", stripped):
modulus_hex.append(stripped.replace(":", ""))
if exponent is None or not modulus_hex:
raise SystemExit(f"xbps-fingerprint.py: not an RSA public key: {pem}")
return int("".join(modulus_hex), 16), exponent
def _mpint(value: int) -> bytes:
"""An SSH mpint: big-endian, with a leading zero when the top bit is set."""
raw = value.to_bytes((value.bit_length() + 7) // 8, "big")
if raw[0] & 0x80:
raw = b"\x00" + raw
return struct.pack(">I", len(raw)) + raw
def fingerprint(pem: Path) -> str:
"""Colon-separated lowercase MD5 of the ssh-rsa blob of the key."""
modulus, exponent = _rsa_numbers(pem)
blob = struct.pack(">I", 7) + b"ssh-rsa" + _mpint(exponent) + _mpint(modulus)
digest = hashlib.md5(blob, usedforsecurity=False).hexdigest()
return ":".join(digest[i : i + 2] for i in range(0, len(digest), 2))
def write_plist(pem: Path, signer: str, out: Path) -> None:
"""Write the client key plist xbps reads from /var/db/xbps/keys."""
modulus, _ = _rsa_numbers(pem)
entry = {
"public-key": pem.read_bytes(),
"public-key-size": modulus.bit_length(),
"signature-by": signer,
}
with out.open("wb") as handle:
plistlib.dump(entry, handle, fmt=plistlib.FMT_XML)
def main(argv: list[str]) -> int:
if len(argv) == 2 and argv[0] == "fingerprint":
print(fingerprint(Path(argv[1])))
return 0
if len(argv) == 4 and argv[0] == "plist":
write_plist(Path(argv[1]), argv[2], Path(argv[3]))
return 0
print(__doc__, file=sys.stderr)
return 2
if __name__ == "__main__":
raise SystemExit(main(sys.argv[1:]))