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.
96 lines
3.2 KiB
Python
Executable File
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:]))
|