#!/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:]))