From b7446afe3baaf1093a229ecdcef56df7cc305476 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20Marczykowski-G=C3=B3recki?= Date: Sat, 6 Feb 2021 04:49:54 +0100 Subject: [PATCH] qvm-template: use key specified in the repo definition if possible This makes the package verified against _only_ the key specified in the repo config, not all the trusted keys. If repo does not specify a key, use the default one (change this to a single file, instead of the whole directory). Existing 'gpgkey' entry pointing at non-existing file will result in an error. --- qubesadmin/tests/tools/qvm_template.py | 14 +++++++- qubesadmin/tools/qvm_template.py | 49 ++++++++++++++++++-------- 2 files changed, 47 insertions(+), 16 deletions(-) diff --git a/qubesadmin/tests/tools/qvm_template.py b/qubesadmin/tests/tools/qvm_template.py index 6a5d6e1..c131a4b 100644 --- a/qubesadmin/tests/tools/qvm_template.py +++ b/qubesadmin/tests/tools/qvm_template.py @@ -152,7 +152,7 @@ class TC_00_qvm_template(qubesadmin.tests.QubesTestCase): ]) self.assertAllCalled() - @mock.patch('qubesadmin.tools.qvm_template.get_keys') + @mock.patch('qubesadmin.tools.qvm_template.get_keys_for_repos') def test_090_install_lock(self, mock_get_keys): class SuccessError(Exception): pass @@ -251,6 +251,8 @@ class TC_00_qvm_template(qubesadmin.tests.QubesTestCase): keyring='/tmp', nogpgcheck=False, cachedir='/var/cache/qvm-template', + repo_files=[], + releasever='4.1', yes=False, allow_pv=False, pool=None @@ -357,6 +359,8 @@ class TC_00_qvm_template(qubesadmin.tests.QubesTestCase): keyring='/tmp', nogpgcheck=False, cachedir='/var/cache/qvm-template', + repo_files=[], + releasever='4.1', yes=False, allow_pv=True, pool='my-pool' @@ -432,6 +436,8 @@ class TC_00_qvm_template(qubesadmin.tests.QubesTestCase): keyring='/tmp', nogpgcheck=False, cachedir='/var/cache/qvm-template', + repo_files=[], + releasever='4.1', yes=False, allow_pv=False, pool=None @@ -496,6 +502,8 @@ class TC_00_qvm_template(qubesadmin.tests.QubesTestCase): keyring='/tmp', nogpgcheck=False, cachedir='/var/cache/qvm-template', + repo_files=[], + releasever='4.1', yes=False, allow_pv=False, pool=None @@ -563,6 +571,8 @@ class TC_00_qvm_template(qubesadmin.tests.QubesTestCase): keyring='/tmp', nogpgcheck=False, cachedir='/var/cache/qvm-template', + repo_files=[], + releasever='4.1', yes=False, allow_pv=False, pool=None @@ -611,6 +621,8 @@ class TC_00_qvm_template(qubesadmin.tests.QubesTestCase): keyring='/tmp', nogpgcheck=False, cachedir='/var/cache/qvm-template', + repo_files=[], + releasever='4.1', yes=False, allow_pv=False, pool=None diff --git a/qubesadmin/tools/qvm_template.py b/qubesadmin/tools/qvm_template.py index b15292e..a9871d5 100644 --- a/qubesadmin/tools/qvm_template.py +++ b/qubesadmin/tools/qvm_template.py @@ -21,6 +21,7 @@ import argparse import collections +import configparser import datetime import enum import fcntl @@ -104,8 +105,10 @@ def get_parser() -> argparse.ArgumentParser: help=('Specify files containing DNF repository configuration.' ' Can be used more than once.')) parser_main.add_argument('--keyring', - default='/etc/qubes/repo-templates/keys', - help='Specify directory containing RPM public keys.') + default='/etc/qubes/repo-templates/keys/RPM-GPG-KEY-qubes-4.1-primary', + help='Specify a file containing default RPM public key. ' + 'Individual repositories may point at repo-specific key ' + 'using \'gpgkey\' option') parser_main.add_argument('--updatevm', default=UPDATEVM, help=('Specify VM to download updates from.' ' (Set to empty string to specify the current VM.)')) @@ -573,18 +576,32 @@ def qrexec_download( raise ConnectionError( "qrexec call 'qubes.TemplateDownload' failed.") -def get_keys(key_dir: str) -> typing.List[str]: - """List gpg keys""" - keys = [] - for name in os.listdir(key_dir): - path = os.path.join(key_dir, name) - if os.path.isfile(path): - keys.append(path) + +def get_keys_for_repos(repo_files: typing.List[str], + releasever: str) -> typing.Dict[str, str]: + """List gpg keys + + Returns a dict reponame -> key path + """ + keys = {} + for repo_file in repo_files: + repo_config = configparser.ConfigParser() + repo_config.read(repo_file) + for repo in repo_config.sections(): + try: + gpgkey_url = repo_config.get(repo, 'gpgkey') + except configparser.NoOptionError: + continue + gpgkey_url = gpgkey_url.replace('$releasever', releasever) + # support only file:// urls + if gpgkey_url.startswith('file://'): + keys[repo] = gpgkey_url[len('file://'):] return keys + def verify_rpm( path: str, - keys: typing.List[str], + key: str, nogpgcheck: bool = False ) -> rpm.hdr: """Verify the digest and signature of a RPM package and return the package @@ -602,9 +619,8 @@ def verify_rpm( """ if not nogpgcheck: with tempfile.TemporaryDirectory() as rpmdb_dir: - for key in keys: - subprocess.check_call( - ['rpmkeys', '--dbpath=' + rpmdb_dir, '--import', key]) + subprocess.check_call( + ['rpmkeys', '--dbpath=' + rpmdb_dir, '--import', key]) try: output = subprocess.check_output( ['rpmkeys', '--dbpath=' + rpmdb_dir, '--checksig', path]) @@ -834,7 +850,7 @@ def install( :param override_existing: Whether to override existing packages. Used for reinstall, upgrade, and downgrade operations """ - keys = get_keys(args.keyring) + keys = get_keys_for_repos(args.repo_files, args.releasever) unverified_rpm_list = [] # rpmfile, reponame verified_rpm_list = [] @@ -847,7 +863,10 @@ def install( else: path = rpmfile - package_hdr = verify_rpm(path, keys, args.nogpgcheck) + repo_key = keys.get(reponame) + if repo_key is None: + repo_key = args.keyring + package_hdr = verify_rpm(path, repo_key, args.nogpgcheck) if not package_hdr: parser.error('Package \'%s\' verification failed.' % rpmfile)