ubuntutools: add pull-* --no-verify-signature option, don't fail if no pub key

Change dsc verification to fail only if the public key was available, but
signature verification failed.  If no public key is available for the dsc,
print warning only. (LP: #1700846)

Also add pull-* parameter --no-verify-signature to manually prevent failure
when signature verification fails.
This commit is contained in:
Dan Streetman 2018-02-28 15:47:53 -05:00
parent 41a6c47ac2
commit 3a413760f3
3 changed files with 82 additions and 41 deletions

View File

@ -129,7 +129,7 @@ class SourcePackage(object):
def __init__(self, package=None, version=None, component=None, def __init__(self, package=None, version=None, component=None,
dscfile=None, lp=None, mirrors=(), workdir='.', quiet=False, dscfile=None, lp=None, mirrors=(), workdir='.', quiet=False,
series=None, pocket=None): series=None, pocket=None, verify_signature=False):
"""Can be initialised using either package or dscfile. """Can be initialised using either package or dscfile.
If package is specified, either the version or series can also be If package is specified, either the version or series can also be
specified; using version will get the specific package version, specified; using version will get the specific package version,
@ -149,6 +149,7 @@ class SourcePackage(object):
self._use_series = True self._use_series = True
self._pocket = pocket self._pocket = pocket
self._dsc_source = dscfile self._dsc_source = dscfile
self._verify_signature = verify_signature
# Cached values: # Cached values:
self._distribution = None self._distribution = None
@ -321,7 +322,7 @@ class SourcePackage(object):
continue continue
yield (bpph.getFileName(), bpph.getUrl(), 0) yield (bpph.getFileName(), bpph.getUrl(), 0)
def pull_dsc(self, verify_signature=False): def pull_dsc(self):
"Retrieve dscfile and parse" "Retrieve dscfile and parse"
if self._dsc_source: if self._dsc_source:
parsed = urlparse(self._dsc_source) parsed = urlparse(self._dsc_source)
@ -333,16 +334,18 @@ class SourcePackage(object):
url = self._lp_url(self.dsc_name) url = self._lp_url(self.dsc_name)
self._download_dsc(url) self._download_dsc(url)
self._check_dsc(verify_signature=verify_signature) self._check_dsc()
def _download_dsc(self, url): def _download_dsc(self, url):
"Download specified dscfile and parse" "Download specified dscfile and parse"
parsed = urlparse(url) parsed = urlparse(url)
if parsed.scheme == 'file': if parsed.scheme == 'file':
with open(parsed.path, 'r') as f: Logger.debug("Using dsc file '%s'" % parsed.path)
with open(parsed.path, 'rb') as f:
body = f.read() body = f.read()
else: else:
try: try:
Logger.debug("Trying dsc url '%s'" % url)
response, body = httplib2.Http().request(url) response, body = httplib2.Http().request(url)
except httplib2.HttpLib2Error as e: except httplib2.HttpLib2Error as e:
raise DownloadError(e) raise DownloadError(e)
@ -351,13 +354,14 @@ class SourcePackage(object):
response.reason)) response.reason))
self._dsc = Dsc(body) self._dsc = Dsc(body)
def _check_dsc(self, verify_signature=False): def _check_dsc(self):
"Check that the dsc matches what we are expecting" "Check that the dsc matches what we are expecting"
assert self._dsc is not None assert self._dsc is not None
self.source = self.dsc['Source'] self.source = self.dsc['Source']
self._version = Version(self.dsc['Version']) self._version = Version(self.dsc['Version'])
valid = False valid = False
no_pub_key = False
message = None message = None
gpg_info = None gpg_info = None
try: try:
@ -381,19 +385,24 @@ class SourcePackage(object):
% (gpg_info['GOODSIG'][1], gpg_info['GOODSIG'][0])) % (gpg_info['GOODSIG'][1], gpg_info['GOODSIG'][0]))
elif 'VALIDSIG' in gpg_info: elif 'VALIDSIG' in gpg_info:
message = 'Valid signature by 0x%s' % gpg_info['VALIDSIG'][0] message = 'Valid signature by 0x%s' % gpg_info['VALIDSIG'][0]
if verify_signature: elif 'NO_PUBKEY' in gpg_info:
no_pub_key = True
message = 'Public key not found, could not verify signature'
if self._verify_signature:
if valid: if valid:
Logger.normal(message) Logger.normal(message)
elif no_pub_key:
Logger.warn(message)
else: else:
Logger.error(message) Logger.error(message)
raise DownloadError(message) raise DownloadError(message)
else: else:
Logger.info(message) Logger.info(message)
def _write_dsc(self, verify_signature=True): def _write_dsc(self):
"Write dsc file to workdir" "Write dsc file to workdir"
if self._dsc is None: if self._dsc is None:
self.pull_dsc(verify_signature=verify_signature) self.pull_dsc()
with open(self.dsc_pathname, 'wb') as f: with open(self.dsc_pathname, 'wb') as f:
f.write(self.dsc.raw_text) f.write(self.dsc.raw_text)
@ -477,9 +486,9 @@ class SourcePackage(object):
return False return False
return True return True
def pull(self, verify_signature=False): def pull(self):
"Pull into workdir" "Pull into workdir"
self._write_dsc(verify_signature=verify_signature) self._write_dsc()
for entry in self.dsc['Files']: for entry in self.dsc['Files']:
name = entry['name'] name = entry['name']
for url in self._source_urls(name): for url in self._source_urls(name):
@ -648,10 +657,10 @@ class DebianSourcePackage(SourcePackage):
continue continue
yield (f.name, f.getUrl(), f.size) yield (f.name, f.getUrl(), f.size)
def pull_dsc(self, verify_signature=True): def pull_dsc(self):
"Retrieve dscfile and parse" "Retrieve dscfile and parse"
try: try:
super(DebianSourcePackage, self).pull_dsc(verify_signature) super(DebianSourcePackage, self).pull_dsc()
return return
except DownloadError: except DownloadError:
pass pass
@ -666,7 +675,7 @@ class DebianSourcePackage(SourcePackage):
break break
else: else:
raise DownloadError('dsc could not be found anywhere') raise DownloadError('dsc could not be found anywhere')
self._check_dsc(verify_signature=verify_signature) self._check_dsc()
# Local methods: # Local methods:
@property @property

View File

@ -83,6 +83,8 @@ def create_argparser(default_pull=None, default_distro=None, default_arch=None):
help='Preferred mirror(s)') help='Preferred mirror(s)')
parser.add_argument('--no-conf', action='store_true', parser.add_argument('--no-conf', action='store_true',
help="Don't read config files or environment variables") help="Don't read config files or environment variables")
parser.add_argument('--no-verify-signature', action='store_true',
help="Don't fail if dsc signature can't be verified")
parser.add_argument('-a', '--arch', default=default_arch, parser.add_argument('-a', '--arch', default=default_arch,
help=help_default_arch) help=help_default_arch)
parser.add_argument('-p', '--pull', default=default_pull, parser.add_argument('-p', '--pull', default=default_pull,
@ -177,6 +179,7 @@ def pull(options):
assert hasattr(options, 'verbose') assert hasattr(options, 'verbose')
assert hasattr(options, 'download_only') assert hasattr(options, 'download_only')
assert hasattr(options, 'no_conf') assert hasattr(options, 'no_conf')
assert hasattr(options, 'no_verify_signature')
# these are type string # these are type string
assert hasattr(options, 'arch') assert hasattr(options, 'arch')
assert hasattr(options, 'pull') assert hasattr(options, 'pull')
@ -234,7 +237,8 @@ def pull(options):
pkgcls = DISTRO_PKG_CLASS[distro] pkgcls = DISTRO_PKG_CLASS[distro]
srcpkg = pkgcls(package=package, version=version, srcpkg = pkgcls(package=package, version=version,
series=release, pocket=pocket, series=release, pocket=pocket,
mirrors=mirrors, dscfile=dscfile) mirrors=mirrors, dscfile=dscfile,
verify_signature=(not options.no_verify_signature))
spph = srcpkg.lp_spph spph = srcpkg.lp_spph
except PackageNotFoundException as e: except PackageNotFoundException as e:
Logger.error(str(e)) Logger.error(str(e))

View File

@ -145,11 +145,14 @@ class LocalSourcePackageTestCase(unittest.TestCase):
return self.request_404(url) return self.request_404(url)
def test_local_copy(self): def test_local_copy(self):
pkg = self.SourcePackage('example', '1.0-1', 'main', pkg = self.SourcePackage(package='example',
version='1.0-1',
component='main',
dscfile='test-data/example_1.0-1.dsc', dscfile='test-data/example_1.0-1.dsc',
workdir=self.workdir) workdir=self.workdir,
verify_signature=False)
pkg.quiet = True pkg.quiet = True
pkg.pull(verify_signature=False) pkg.pull()
pkg.unpack() pkg.unpack()
def test_workdir_srcpkg_noinfo(self): def test_workdir_srcpkg_noinfo(self):
@ -159,9 +162,10 @@ class LocalSourcePackageTestCase(unittest.TestCase):
pkg = self.SourcePackage(dscfile=os.path.join(self.workdir, pkg = self.SourcePackage(dscfile=os.path.join(self.workdir,
'example_1.0-1.dsc'), 'example_1.0-1.dsc'),
workdir=self.workdir) workdir=self.workdir,
verify_signature=False)
pkg.quiet = True pkg.quiet = True
pkg.pull(verify_signature=False) pkg.pull()
pkg.unpack() pkg.unpack()
def test_workdir_srcpkg_info(self): def test_workdir_srcpkg_info(self):
@ -169,12 +173,14 @@ class LocalSourcePackageTestCase(unittest.TestCase):
shutil.copy2('test-data/example_1.0.orig.tar.gz', self.workdir) shutil.copy2('test-data/example_1.0.orig.tar.gz', self.workdir)
shutil.copy2('test-data/example_1.0-1.debian.tar.xz', self.workdir) shutil.copy2('test-data/example_1.0-1.debian.tar.xz', self.workdir)
pkg = self.SourcePackage('example', '1.0-1', 'main', pkg = self.SourcePackage(package='example', version='1.0-1',
component='main',
dscfile=os.path.join(self.workdir, dscfile=os.path.join(self.workdir,
'example_1.0-1.dsc'), 'example_1.0-1.dsc'),
workdir=self.workdir) workdir=self.workdir,
verify_signature=False)
pkg.quiet = True pkg.quiet = True
pkg.pull(verify_signature=False) pkg.pull()
pkg.unpack() pkg.unpack()
def test_verification(self): def test_verification(self):
@ -185,19 +191,25 @@ class LocalSourcePackageTestCase(unittest.TestCase):
'r+b') as f: 'r+b') as f:
f.write(b'CORRUPTION') f.write(b'CORRUPTION')
pkg = self.SourcePackage('example', '1.0-1', 'main', pkg = self.SourcePackage(package='example',
version='1.0-1',
component='main',
dscfile='test-data/example_1.0-1.dsc', dscfile='test-data/example_1.0-1.dsc',
workdir=self.workdir) workdir=self.workdir,
verify_signature=False)
pkg.quiet = True pkg.quiet = True
pkg.pull(verify_signature=False) pkg.pull()
def test_pull(self): def test_pull(self):
pkg = self.SourcePackage('example', '1.0-1', 'main', pkg = self.SourcePackage(package='example',
workdir=self.workdir) version='1.0-1',
component='main',
workdir=self.workdir,
verify_signature=False)
pkg.url_opener = self.url_opener pkg.url_opener = self.url_opener
pkg.quiet = True pkg.quiet = True
pkg.pull(verify_signature=False) pkg.pull()
def test_mirrors(self): def test_mirrors(self):
mirror = 'http://mirror' mirror = 'http://mirror'
@ -209,15 +221,21 @@ class LocalSourcePackageTestCase(unittest.TestCase):
url_opener = mock.MagicMock(spec=OpenerDirector) url_opener = mock.MagicMock(spec=OpenerDirector)
url_opener.open.side_effect = _callable_iter url_opener.open.side_effect = _callable_iter
pkg = self.SourcePackage('example', '1.0-1', 'main', pkg = self.SourcePackage(package='example',
workdir=self.workdir, mirrors=[mirror]) version='1.0-1',
component='main',
workdir=self.workdir,
mirrors=[mirror],
verify_signature=False)
pkg.url_opener = url_opener pkg.url_opener = url_opener
pkg.quiet = True pkg.quiet = True
pkg.pull(verify_signature=False) pkg.pull()
def test_dsc_missing(self): def test_dsc_missing(self):
self.mock_http.side_effect = self.request_404 self.mock_http.side_effect = self.request_404
pkg = self.SourcePackage('example', '1.0-1', 'main', pkg = self.SourcePackage(package='example',
version='1.0-1',
component='main',
workdir=self.workdir) workdir=self.workdir)
pkg.quiet = True pkg.quiet = True
self.assertRaises(ubuntutools.archive.DownloadError, pkg.pull) self.assertRaises(ubuntutools.archive.DownloadError, pkg.pull)
@ -243,12 +261,15 @@ class DebianLocalSourcePackageTestCase(LocalSourcePackageTestCase):
url_opener = mock.MagicMock(spec=OpenerDirector) url_opener = mock.MagicMock(spec=OpenerDirector)
url_opener.open.side_effect = _callable_iter url_opener.open.side_effect = _callable_iter
pkg = self.SourcePackage('example', '1.0-1', 'main', pkg = self.SourcePackage(package='example',
workdir=self.workdir, mirrors=[debian_mirror, version='1.0-1',
debsec_mirror]) component='main',
workdir=self.workdir,
mirrors=[debian_mirror, debsec_mirror],
verify_signature=False)
pkg.quiet = True pkg.quiet = True
pkg.url_opener = url_opener pkg.url_opener = url_opener
pkg.pull(verify_signature=False) pkg.pull()
pkg.unpack() pkg.unpack()
def test_dsc_missing(self): def test_dsc_missing(self):
@ -262,10 +283,14 @@ class DebianLocalSourcePackageTestCase(LocalSourcePackageTestCase):
'[GNUPG:] GOODSIG DEADBEEF Joe Developer ' '[GNUPG:] GOODSIG DEADBEEF Joe Developer '
'<joe@example.net>') '<joe@example.net>')
pkg = self.SourcePackage('example', '1.0-1', 'main', pkg = self.SourcePackage(package='example',
workdir=self.workdir, mirrors=[mirror]) version='1.0-1',
component='main',
workdir=self.workdir,
mirrors=[mirror],
verify_signature=False)
pkg.url_opener = self.url_opener pkg.url_opener = self.url_opener
pkg.pull(verify_signature=False) pkg.pull()
def test_dsc_badsig(self): def test_dsc_badsig(self):
mirror = 'http://mirror' mirror = 'http://mirror'
@ -277,8 +302,11 @@ class DebianLocalSourcePackageTestCase(LocalSourcePackageTestCase):
mock_gpg_info.return_value = debian.deb822.GpgInfo.from_output( mock_gpg_info.return_value = debian.deb822.GpgInfo.from_output(
'[GNUPG:] ERRSIG DEADBEEF') '[GNUPG:] ERRSIG DEADBEEF')
pkg = self.SourcePackage('example', '1.0-1', 'main', pkg = self.SourcePackage(package='example',
workdir=self.workdir, mirrors=[mirror]) version='1.0-1',
component='main',
workdir=self.workdir,
mirrors=[mirror])
try: try:
self.assertRaises(ubuntutools.archive.DownloadError, pkg.pull) self.assertRaises(ubuntutools.archive.DownloadError, pkg.pull)
except URLError: except URLError: