Skip to content

Commit

Permalink
"signature" is stored for RPMs, SRPMs, and DRPMs.
Browse files Browse the repository at this point in the history
closes #1156
https://pulp.plan.io/issues/1156

During upload or sync of a package, the signature is extracted from
the package header and is stored as an attribute for rpms/srpms/drms.
  • Loading branch information
ipanova committed Jun 24, 2016
1 parent e93f53d commit 7104166
Show file tree
Hide file tree
Showing 7 changed files with 100 additions and 27 deletions.
4 changes: 4 additions & 0 deletions plugins/pulp_rpm/plugins/db/models.py
Expand Up @@ -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
Expand All @@ -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()
Expand Down
11 changes: 9 additions & 2 deletions plugins/pulp_rpm/plugins/importers/yum/listener.py
Expand Up @@ -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__)
Expand Down Expand Up @@ -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):
Expand All @@ -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()
53 changes: 53 additions & 0 deletions 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
Expand Down Expand Up @@ -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:]
12 changes: 2 additions & 10 deletions plugins/pulp_rpm/plugins/importers/yum/upload.py
Expand Up @@ -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]
Expand Down Expand Up @@ -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

Expand Down
Empty file added plugins/test/data/fake.rpm
Empty file.
28 changes: 27 additions & 1 deletion 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):
"""
Expand All @@ -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')
19 changes: 5 additions & 14 deletions plugins/test/unit/plugins/importers/yum/test_listener.py
Expand Up @@ -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
Expand All @@ -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):
Expand Down

0 comments on commit 7104166

Please sign in to comment.