Files
voculinux/packaging/void/xbps-fingerprint.py
T
xavierk b47a65c91e
CI / security (push) Successful in 1m15s
CI / lint (push) Successful in 23s
CI / ai-review (push) Skipped
fix(void): build the xbps release from the tag, not an attached sdist (#37)
Every release is built from its tag alone by packaging/void/build-release.sh, shared by the tag workflow and local dry runs. Signs in a network-less container with the key mounted read-only and publishes to the tag release and the rolling latest release.
2026-11-06 15:31:47 +00:00

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:]))