Support signing of rpm wrapped live images

With this patch, you can specify a command for
signing of koji builds. For example:

    signing_key_password_file = '~/file_with_password_for_key_fedora-24'
    signing_key_id = '81b46521'
    signing_command = '~/git/releng/scripts/sigulsign_unsigned.py -vv --password=%(signing_key_password)s fedora-24'

'signing_key_password_file' is a path to a file which contains
a password that will be formatted into 'signing_command' string
via '%(signing_key_password)s' string format syntax (if used).
Because pungi config is usualy stored in git and part of compose
logs we don't want password to be included directly in the config.
Note: If '-' is used instead of a filename, then you will be asked
for the password interactivelly right after pungi starts.

'signing_key_id' is ID of the key that will be used for the signing.
This ID will be used when crafting koji paths to signed files
(kojipkgs.fedoraproject.org/packages/NAME/VER/REL/data/signed/KEYID/..).

'signing_command' a command that will be run with a build as a single
argument. This command mustn't require any user interaction.
If you need to pass a password for a signing key to the command,
do this via command line option of the command with use of string
formatting syntax '%(signing_key_password)s' (see details
about 'signing_key_password_file').

Signed-off-by: Tomáš Mlčoch <tmlcoch@redhat.com>
This commit is contained in:
Tomas Mlcoch 2016-02-12 13:00:19 +01:00
parent be4d596c36
commit 5bffca5037
3 changed files with 143 additions and 1 deletions

View File

@ -253,6 +253,43 @@ def run_compose(compose):
print(i)
sys.exit(1)
# PREP
# Note: This may be put into a new method of phase classes (e.g. .prep())
# in same way as .validate() or .run()
# Prep for liveimages - Obtain a password for signing rpm wrapped images
if ("signing_key_password_file" in compose.conf
and "signing_command" in compose.conf
and "%(signing_key_password)s" in compose.conf["signing_command"]
and not liveimages_phase.skip()):
# TODO: Don't require key if signing is turned off
# Obtain signing key password
signing_key_password = None
# Use appropriate method
if compose.conf["signing_key_password_file"] == "-":
# Use stdin (by getpass module)
try:
signing_key_password = getpass.getpass("Signing key password: ")
except EOFError:
compose.log_debug("Ignoring signing key password")
pass
else:
# Use text file with password
try:
signing_key_password = open(compose.conf["signing_key_password_file"], "r").readline().rstrip('\n')
except IOError:
# Filename is not print intentionally in case someone puts password directly into the option
err_msg = "Cannot load password from file specified by 'signing_key_password_file' option"
compose.log_error(err_msg)
print(err_msg)
sys.exit(1)
if signing_key_password:
# Store the password
compose.conf["signing_key_password"] = signing_key_password
# INIT phase
init_phase.start()
init_phase.stop()

View File

@ -22,7 +22,7 @@ import pipes
import shutil
from kobo.threads import ThreadPool, WorkerThread
from kobo.shortcuts import run
from kobo.shortcuts import run, save_to_file
from pungi.wrappers.kojiwrapper import KojiWrapper
from pungi.wrappers.iso import IsoWrapper
@ -51,6 +51,21 @@ class LiveImagesPhase(PhaseBase):
"expected_types": [list],
"optional": True,
},
{
"name": "signing_key_id",
"expected_types": [str],
"optional": True,
},
{
"name": "signing_key_password_file",
"expected_types": [str],
"optional": True,
},
{
"name": "signing_command",
"expected_types": [str],
"optional": True,
},
)
def __init__(self, compose):
@ -102,6 +117,7 @@ class LiveImagesPhase(PhaseBase):
"ksurl": None,
"specfile": None,
"scratch": False,
"sign": False,
"label": "", # currently not used
}
@ -129,6 +145,10 @@ class LiveImagesPhase(PhaseBase):
# For other images is scratch always on
cmd["scratch"] = data.get("scratch", False)
# Signing of the rpm wrapped image
if not cmd["scratch"] and data.get("sign"):
cmd["sign"] = True
format = "%(compose_id)s-%(variant)s-%(arch)s-%(disc_type)s%(disc_num)s%(suffix)s"
# Custom name (prefix)
if cmd["name"]:
@ -225,6 +245,14 @@ class CreateLiveImageThread(WorkerThread):
# copy finished rpm to isos/ (if rpm wrapped ISO was built)
if cmd["specfile"]:
rpm_paths = koji_wrapper.get_wrapped_rpm_path(output["task_id"])
if cmd["sign"]:
# Sign the rpm wrapped images and get their paths
compose.log_info("Signing rpm wrapped images in task_id: %s (expected key ID: %s)" % (output["task_id"], compose.conf.get("signing_key_id")))
signed_rpm_paths = self._sign_image(koji_wrapper, compose, cmd, output["task_id"])
if signed_rpm_paths:
rpm_paths = signed_rpm_paths
for rpm_path in rpm_paths:
shutil.copy2(rpm_path, cmd["wrapped_rpms_path"])
@ -240,3 +268,78 @@ class CreateLiveImageThread(WorkerThread):
dir, filename = os.path.split(iso_path)
iso = IsoWrapper()
run("cd %s && %s" % (pipes.quote(dir), iso.get_manifest_cmd(filename)))
def _sign_image(self, koji_wrapper, compose, cmd, koji_task_id):
signing_key_id = compose.conf.get("signing_key_id")
signing_command = compose.conf.get("signing_command")
if not signing_key_id:
compose.log_warning("Signing is enabled but signing_key_id is not specified")
compose.log_warning("Signing skipped")
return None
if not signing_command:
compose.log_warning("Signing is enabled but signing_command is not specified")
compose.log_warning("Signing skipped")
return None
# Prepare signing log file
signing_log_file = compose.paths.log.log_file(cmd["build_arch"], "live_images-signing-%s" % os.path.basename(cmd["iso_path"]))
# Sign the rpm wrapped images
try:
sign_builds_in_task(koji_wrapper, koji_task_id, signing_command, log_file=signing_log_file, signing_key_password=compose.conf.get("signing_key_password"))
except RuntimeError:
compose.log_error("Error while signing rpm wrapped images. See log: %s" % signing_log_file)
raise
# Get pats to the signed rpms
signing_key_id = signing_key_id.lower() # Koji uses lowercase in paths
rpm_paths = koji_wrapper.get_signed_wrapped_rpms_paths(koji_task_id, signing_key_id)
# Wait untill files are available
if wait_paths(rpm_paths, 60*15):
# Files are ready
return rpm_paths
# Signed RPMs are not available
compose.log_warning("Signed files are not available: %s" % rpm_paths)
compose.log_warning("Unsigned files will be used")
return None
def wait_paths(paths, timeout=60):
started = time.time()
remaining = paths[:]
while True:
for path in remaining[:]:
if os.path.exists(path):
remaining.remove(path)
if not remaining:
break
time.sleep(1)
if timeout >= 0 and (time.time() - started) > timeout:
return False
return True
def sign_builds_in_task(koji_wrapper, task_id, signing_command, log_file=None, signing_key_password=None):
# Get list of nvrs that should be signed
nvrs = koji_wrapper.get_build_nvrs(task_id)
if not nvrs:
# No builds are available (scratch build, etc.?)
return
# Append builds to sign_cmd
for nvr in nvrs:
signing_command += " '%s'" % nvr
# Log signing command before password is filled in it
if log_file:
save_to_file(log_file, signing_command, append=True)
# Fill password into the signing command
if signing_key_password:
signing_command = signing_command % {"signing_key_password": signing_key_password}
# Sign the builds
run(signing_command, can_fail=False, show_cmd=False, logfile=log_file)

View File

@ -95,6 +95,7 @@ class TestLiveImagesPhase(unittest.TestCase):
'iso_path': '/iso_dir/amd64/Client/image-name',
'version': None,
'specfile': None,
'sign': False,
'type': 'live',
'ksurl': None},
compose.variants['Client'],
@ -140,6 +141,7 @@ class TestLiveImagesPhase(unittest.TestCase):
'iso_path': '/iso_dir/amd64/Client/image-name',
'version': None,
'specfile': None,
'sign': False,
'type': 'appliance',
'ksurl': 'https://git.example.com/kickstarts.git?#CAFEBABE'},
compose.variants['Client'],