mirror of
https://github.com/XRPLF/rippled.git
synced 2026-10-08 12:48:14 +00:00
131 lines
4.4 KiB
Python
Executable File
131 lines
4.4 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""Sign the RPMs built by build_pkg.py.
|
|
|
|
Nexus signs the yum repository metadata (via the 'rpm-<channel>' group
|
|
repository), but never the packages themselves, so they carry their own
|
|
signature. Clients verify the packages with gpgcheck=1 and the metadata with
|
|
repo_gpgcheck=1.
|
|
|
|
The DEBs are deliberately not signed: embedded DEB signatures exist (debsigs),
|
|
but apt does not verify them by default and trusts the repository metadata,
|
|
which Nexus signs, instead.
|
|
|
|
PKG_SIGNING_KEY is read from the environment, so the key never reaches the
|
|
process list.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import os
|
|
import subprocess
|
|
import tempfile
|
|
from pathlib import Path
|
|
|
|
# An RSA signature lands in the RSAHEADER tag, a DSA or EdDSA one in DSAHEADER,
|
|
# so both are queried; checking only the first would reject a signed package.
|
|
SIGNATURE_QUERY = "%{RSAHEADER:pgpsig}%{DSAHEADER:pgpsig}"
|
|
UNSIGNED = "(none)(none)"
|
|
|
|
|
|
def gpg(gnupghome: Path, *args: str, stdin: str | None = None) -> str:
|
|
"""Run gpg against a throwaway keyring and return its stdout."""
|
|
return subprocess.run(
|
|
["gpg", "--batch", "--quiet", *args],
|
|
input=stdin,
|
|
# stderr is left alone so a failing gpg explains itself.
|
|
stdout=subprocess.PIPE,
|
|
text=True,
|
|
check=True,
|
|
env={**os.environ, "GNUPGHOME": str(gnupghome)},
|
|
).stdout
|
|
|
|
|
|
def import_key(gnupghome: Path, key: str) -> str:
|
|
"""Import the armoured private key and return its fingerprint."""
|
|
gpg(gnupghome, "--import", stdin=key)
|
|
|
|
records = [
|
|
line.split(":")
|
|
for line in gpg(gnupghome, "--list-secret-keys", "--with-colons").splitlines()
|
|
]
|
|
# Exactly one, so the fingerprint picked below is not a guess.
|
|
secrets = [record for record in records if record[0] == "sec"]
|
|
assert (
|
|
len(secrets) == 1
|
|
), f"PKG_SIGNING_KEY must hold exactly one secret key, found {len(secrets)}"
|
|
|
|
# The first fingerprint belongs to the primary key; subkeys follow.
|
|
fingerprints = [record[9] for record in records if record[0] == "fpr"]
|
|
assert fingerprints, "PKG_SIGNING_KEY holds a secret key with no fingerprint"
|
|
return fingerprints[0]
|
|
|
|
|
|
def sign(gnupghome: Path, rpms: list[Path], fingerprint: str) -> None:
|
|
"""Attach a signature to every RPM in one rpmsign invocation."""
|
|
subprocess.run(
|
|
[
|
|
"rpmsign",
|
|
"--define",
|
|
f"_gpg_name {fingerprint}",
|
|
# Loopback pinentry: the key is unattended, so there is no tty to
|
|
# prompt on.
|
|
"--define",
|
|
"_gpg_sign_cmd_extra_args --pinentry-mode loopback --batch --yes",
|
|
"--addsign",
|
|
*(str(rpm) for rpm in rpms),
|
|
],
|
|
check=True,
|
|
env={**os.environ, "GNUPGHOME": str(gnupghome)},
|
|
)
|
|
|
|
|
|
def verify(rpms: list[Path]) -> None:
|
|
"""Fail unless every RPM now carries a signature.
|
|
|
|
rpmsign can exit 0 having attached nothing, and an unsigned package is only
|
|
rejected later, on the installing machine.
|
|
"""
|
|
for rpm in rpms:
|
|
signature = subprocess.run(
|
|
["rpm", "--query", "--queryformat", SIGNATURE_QUERY, "--package", str(rpm)],
|
|
stdout=subprocess.PIPE,
|
|
text=True,
|
|
check=True,
|
|
).stdout.strip()
|
|
assert signature != UNSIGNED, f"{rpm} is unsigned after rpmsign"
|
|
|
|
|
|
def main() -> None:
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
parser.add_argument(
|
|
"--package-dir",
|
|
type=Path,
|
|
default=Path("build"),
|
|
help="searched recursively for *.rpm (default: %(default)s)",
|
|
)
|
|
args = parser.parse_args()
|
|
package_dir: Path = args.package_dir
|
|
|
|
# Deliberately not shared with publish_pkg.py, which ships standalone in the
|
|
# packaging image.
|
|
rpms = sorted(path for path in package_dir.rglob("*.rpm") if path.is_file())
|
|
# Signing nothing would otherwise look like a successful signing.
|
|
assert rpms, f"no RPMs found in {package_dir}"
|
|
|
|
key = os.environ.get("PKG_SIGNING_KEY")
|
|
assert key, "PKG_SIGNING_KEY is required"
|
|
|
|
# The keyring holds an unencrypted private key, so it goes even if signing
|
|
# fails.
|
|
with tempfile.TemporaryDirectory() as tmp:
|
|
gnupghome = Path(tmp)
|
|
fingerprint = import_key(gnupghome, key)
|
|
print(f"Signing {len(rpms)} RPM(s) with {fingerprint}.")
|
|
sign(gnupghome, rpms, fingerprint)
|
|
verify(rpms)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|