diff --git a/plugins/pulp_rpm/plugins/db/models.py b/plugins/pulp_rpm/plugins/db/models.py index 72ccf6bbe..49e9ac4cc 100644 --- a/plugins/pulp_rpm/plugins/db/models.py +++ b/plugins/pulp_rpm/plugins/db/models.py @@ -117,6 +117,9 @@ class NonMetadataPackage(UnitMixin, FileContentUnit): :ivar checksum: The checksum of the package. :type checksum: mongoengine.StringField + :ivar signature: The signature of the package. + :type signature: mongoengine.StringField + :ivar version_sort_index: ??? :type version_sort_index: mongoengine.StringField @@ -129,6 +132,7 @@ class NonMetadataPackage(UnitMixin, FileContentUnit): checksum = mongoengine.StringField(required=True) checksumtype = ChecksumTypeStringField(required=True) checksums = mongoengine.DictField() + signature = mongoengine.StringField() # We generate these two version_sort_index = mongoengine.StringField() diff --git a/plugins/pulp_rpm/plugins/importers/yum/listener.py b/plugins/pulp_rpm/plugins/importers/yum/listener.py index 3bea02758..53fbf2cc2 100644 --- a/plugins/pulp_rpm/plugins/importers/yum/listener.py +++ b/plugins/pulp_rpm/plugins/importers/yum/listener.py @@ -5,6 +5,7 @@ from pulp.server import util from pulp_rpm.common import constants +from pulp_rpm.plugins.importers.yum.parse import rpm as rpm_parse _logger = logging.getLogger(__name__) @@ -183,7 +184,7 @@ def _validate_checksumtype(self, unit): class RPMListener(PackageListener): """ - The RPM package download lister. + The RPM/SRPM/DRPM package download listener. """ def download_succeeded(self, report): @@ -194,8 +195,14 @@ def download_succeeded(self, report): except (verification.VerificationException, util.InvalidChecksumType): # verification failed, unit not added return + + # we need to read package header in order to extract signature info + # unit is not added if header cannot be read + headers = rpm_parse.package_headers(report.destination) + added_unit = self.sync.add_rpm_unit(self.metadata_files, unit) added_unit.safe_import_content(report.destination) if not added_unit.downloaded: added_unit.downloaded = True - added_unit.save() + added_unit['signature'] = rpm_parse.package_signature(headers) + added_unit.save() diff --git a/plugins/pulp_rpm/plugins/importers/yum/parse/rpm.py b/plugins/pulp_rpm/plugins/importers/yum/parse/rpm.py index 8e51b216b..de49fec9e 100644 --- a/plugins/pulp_rpm/plugins/importers/yum/parse/rpm.py +++ b/plugins/pulp_rpm/plugins/importers/yum/parse/rpm.py @@ -1,5 +1,7 @@ +from __future__ import absolute_import import logging import os +import rpm as rpm_module from createrepo import yumbased from pulp.server import util @@ -90,3 +92,54 @@ def string_to_unicode(data): except UnicodeError: # try others continue + + +def package_headers(filename): + """ + Return package header from rpm/srpm/drpm. + + :param filename: full path to the package to analyze + :type filename: str + + :return: package header + :rtype: rpm.hdr + """ + + # Read the RPM header attributes for use later + ts = rpm_module.TransactionSet() + ts.setVSFlags(rpm_module._RPMVSF_NOSIGNATURES) + fd = os.open(filename, os.O_RDONLY) + try: + headers = ts.hdrFromFdno(fd) + os.close(fd) + except rpm_module.error: + # Raised if the headers cannot be read + os.close(fd) + raise + + return headers + + +def package_signature(headers): + """ + Extract package signature from rpm/srpm/drpm. + + :param headers: package header + :type headers: rpm.hdr + + :return: package signature + :rtype: str + """ + + # this expression looks up at the header that was encrypted whether with RSA or DSA algorithm, + # then extracts all the possible signature tags, so the signature can be none, signed with a + # gpg or pgp key. + # this exact expression is also used in the yum code + # https://github.com/rpm-software-management/yum/blob/master/rpmUtils/miscutils.py#L105 + signature = headers.sprintf("%|DSAHEADER?{%{DSAHEADER:pgpsig}}:" + "{%|RSAHEADER?{%{RSAHEADER:pgpsig}}:{%|SIGGPG?{%{SIGGPG:pgpsig}}:" + "{%|SIGPGP?{%{SIGPGP:pgpsig}}:{none}|}|}|}|") + if signature == "none": + return None + # gpg program uses the last 8 characters of the fingerprint + return signature.split()[-1][-8:] diff --git a/plugins/pulp_rpm/plugins/importers/yum/upload.py b/plugins/pulp_rpm/plugins/importers/yum/upload.py index 965aaa4e0..e10a15f00 100644 --- a/plugins/pulp_rpm/plugins/importers/yum/upload.py +++ b/plugins/pulp_rpm/plugins/importers/yum/upload.py @@ -458,16 +458,7 @@ def _extract_rpm_data(type_id, rpm_filename): rpm_data = dict() # Read the RPM header attributes for use later - ts = rpm.TransactionSet() - ts.setVSFlags(rpm._RPMVSF_NOSIGNATURES) - fd = os.open(rpm_filename, os.O_RDONLY) - try: - headers = ts.hdrFromFdno(fd) - os.close(fd) - except rpm.error: - # Raised if the headers cannot be read - os.close(fd) - raise + headers = rpm_parse.package_headers(rpm_filename) for k in ['name', 'version', 'release', 'epoch']: rpm_data[k] = headers[k] @@ -520,6 +511,7 @@ def _extract_rpm_data(type_id, rpm_filename): # rpm_parse.get_package_xml(..) file_stat = os.stat(rpm_filename) rpm_data['time'] = file_stat[stat.ST_MTIME] + rpm_data['signature'] = rpm_parse.package_signature(headers) return rpm_data diff --git a/plugins/test/data/fake.rpm b/plugins/test/data/fake.rpm new file mode 100644 index 000000000..e69de29bb diff --git a/plugins/test/unit/plugins/importers/yum/parse/test_rpm.py b/plugins/test/unit/plugins/importers/yum/parse/test_rpm.py index 0a4221c9f..2def97099 100644 --- a/plugins/test/unit/plugins/importers/yum/parse/test_rpm.py +++ b/plugins/test/unit/plugins/importers/yum/parse/test_rpm.py @@ -1,10 +1,16 @@ -import unittest +from __future__ import absolute_import +import os + +import rpm as rpm_module from mock import patch, Mock +from pulp.common.compat import unittest from pulp.devel.unit import util from pulp_rpm.plugins.importers.yum.parse import rpm +DATA_DIR = os.path.join(os.path.dirname(__file__), '../../../../../data') + class TesGetPackageXml(unittest.TestCase): """ @@ -30,3 +36,23 @@ def test_non_supported_encoding(self): start_string.decode.side_effect = UnicodeError() result_string = rpm.string_to_unicode(start_string) self.assertEquals(None, result_string) + + +class PackageHeaders(unittest.TestCase): + """ + tests for package headers and signature extraction + """ + + def test_package_signature_from_header(self): + sample_rpm_filename = os.path.join(DATA_DIR, 'walrus-5.21-1.noarch.rpm') + headers = rpm.package_headers(sample_rpm_filename) + self.assertTrue(isinstance(headers, rpm_module.hdr)) + signature = rpm.package_signature(headers) + self.assertEquals(signature, 'f78fb195') + self.assertEquals(len(signature), 8) + + def test_invalid_package_headers(self): + fake_rpm_file = os.path.join(DATA_DIR, 'fake.rpm') + with self.assertRaises(rpm_module.error) as e: + rpm.package_headers(fake_rpm_file) + self.assertEquals(e.exception.message, 'error reading package header') diff --git a/plugins/test/unit/plugins/importers/yum/test_listener.py b/plugins/test/unit/plugins/importers/yum/test_listener.py index ff5cd4f9a..9e9407d30 100644 --- a/plugins/test/unit/plugins/importers/yum/test_listener.py +++ b/plugins/test/unit/plugins/importers/yum/test_listener.py @@ -31,7 +31,8 @@ def test_calls_deleting(self, mock_deleting): # it was used as a context manager self.assertEqual(mock_deleting.return_value.__exit__.call_count, 1) - def test_change_download_flag(self): + @mock.patch('pulp_rpm.plugins.importers.yum.listener.rpm_parse') + def test_change_download_flag(self, mock_rpm_parse): unit = mock.MagicMock() unit.checksumtype = 'sha256' self.report.data = unit @@ -44,19 +45,9 @@ def test_change_download_flag(self): # test flag changed to True and save was called self.assertEqual(added_unit.downloaded, True) self.assertEqual(added_unit.save.call_count, 1) - - def test_save_not_called(self): - unit = mock.MagicMock() - self.report.data = unit - added_unit = mock.MagicMock() - added_unit.downloaded = True - self.mock_sync.add_rpm_unit.return_value = added_unit - - self.listener.download_succeeded(self.report) - - # test flag is still set to True but save was not called - self.assertEqual(added_unit.downloaded, True) - self.assertEqual(added_unit.save.call_count, 0) + mock_rpm_parse.package_headers.assert_called_once_with('/a/b/c') + headers = mock_rpm_parse.package_headers.return_value + mock_rpm_parse.package_signature.assert_called_once_with(headers) class TestPackageListener(unittest.TestCase):