Browse Source

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.
Marek Marczykowski-Górecki 3 years ago
parent
commit
b7446afe3b
2 changed files with 47 additions and 16 deletions
  1. 13 1
      qubesadmin/tests/tools/qvm_template.py
  2. 34 15
      qubesadmin/tools/qvm_template.py

+ 13 - 1
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

+ 34 - 15
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)