Merge lp:~stub/charm-helpers/fix-configure_sources into lp:charm-helpers

Proposed by Stuart Bishop
Status: Merged
Merged at revision: 196
Proposed branch: lp:~stub/charm-helpers/fix-configure_sources
Merge into: lp:charm-helpers
Diff against target: 118 lines (+64/-21)
2 files modified
charmhelpers/fetch/__init__.py (+17/-3)
tests/fetch/test_fetch.py (+47/-18)
To merge this branch: bzr merge lp:~stub/charm-helpers/fix-configure_sources
Reviewer Review Type Date Requested Status
David Britton (community) Approve
Review via email: mp+226684@code.launchpad.net

Description of the change

fetch.add_source() is blindly retrieving a key by GPG key id from a remote keyserver, which is in most cases insecure.

This branch addresses this security problem by allowing add_source() to accept a full GPG key, rather than just a GPG key id. This is Bug #1341527.

I've also fixed Bug #1269718 while I'm here. I elected to raise an exception rather than log an error and continue, as other parts of the code where already raising exceptions when invalid sources are requested.

To post a comment you must log in.
Revision history for this message
Chris Glass (tribaal) wrote :

That is a great addition to the mechanism IMO.

Could you please add a short docstring to the function explaining what the key parameter should be like? Example:

"""This function adds an entry to the sources.list file.

@param source: The archive's URL. Example: "http://archive.ubuntu.com"
@param key: The GPG key to verify the archive packages with. Can be either a
    key ID or a full public key block. If only the key ID is specified the
    key will be fetched from the network via HKP."""

Revision history for this message
David Britton (dpb) wrote :

+1, tested and works great. Thanks for adding this.

Agreed with @tribaal on the docstring. Now that the functionality is expanding, I think it sort of needs the docstring, and it's a good chance to add it.

Marking as approved since this is a minor non-functional change suggested.

review: Approve
165. By Stuart Bishop

Add docstrings

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'charmhelpers/fetch/__init__.py'
2--- charmhelpers/fetch/__init__.py 2014-06-27 08:11:50 +0000
3+++ charmhelpers/fetch/__init__.py 2014-07-14 14:54:10 +0000
4@@ -1,4 +1,5 @@
5 import importlib
6+from tempfile import NamedTemporaryFile
7 import time
8 from yaml import safe_load
9 from charmhelpers.core.host import (
10@@ -225,10 +226,23 @@
11 release = lsb_release()['DISTRIB_CODENAME']
12 with open('/etc/apt/sources.list.d/proposed.list', 'w') as apt:
13 apt.write(PROPOSED_POCKET.format(release))
14+ else:
15+ raise SourceConfigError("Unknown source: {!r}".format(source))
16+
17 if key:
18- subprocess.check_call(['apt-key', 'adv', '--keyserver',
19- 'hkp://keyserver.ubuntu.com:80', '--recv',
20- key])
21+ if '-----BEGIN PGP PUBLIC KEY BLOCK-----' in key:
22+ with NamedTemporaryFile() as key_file:
23+ key_file.write(key)
24+ key_file.flush()
25+ key_file.seek(0)
26+ subprocess.check_call(['apt-key', 'add', '-'], stdin=key_file)
27+ else:
28+ # Note that hkp: is in no way a secure protocol. Using a
29+ # GPG key id is pointless from a security POV unless you
30+ # absolutely trust your network and DNS.
31+ subprocess.check_call(['apt-key', 'adv', '--keyserver',
32+ 'hkp://keyserver.ubuntu.com:80', '--recv',
33+ key])
34
35
36 def configure_sources(update=False,
37
38=== modified file 'tests/fetch/test_fetch.py'
39--- tests/fetch/test_fetch.py 2014-06-05 10:47:46 +0000
40+++ tests/fetch/test_fetch.py 2014-07-14 14:54:10 +0000
41@@ -1,3 +1,4 @@
42+from cStringIO import StringIO
43 import subprocess
44
45 from tests.helpers import patch_open
46@@ -177,26 +178,54 @@
47 mock_file.write.assert_called_with(result)
48
49 @patch('subprocess.check_call')
50+ def test_add_source_http_and_key_id(self, check_call):
51+ source = "http://archive.ubuntu.com/ubuntu raring-backports main"
52+ key_id = "akey"
53+ fetch.add_source(source=source, key=key_id)
54+ check_call.assert_has_calls([
55+ call(['add-apt-repository', '--yes', source]),
56+ call(['apt-key', 'adv', '--keyserver',
57+ 'hkp://keyserver.ubuntu.com:80', '--recv', key_id])
58+ ])
59+
60+ @patch('subprocess.check_call')
61+ def test_add_source_https_and_key_id(self, check_call):
62+ source = "https://USER:PASS@private-ppa.launchpad.net/project/awesome"
63+ key_id = "GPGPGP"
64+ fetch.add_source(source=source, key=key_id)
65+ check_call.assert_has_calls([
66+ call(['add-apt-repository', '--yes', source]),
67+ call(['apt-key', 'adv', '--keyserver',
68+ 'hkp://keyserver.ubuntu.com:80', '--recv', key_id])
69+ ])
70+
71+ @patch('subprocess.check_call')
72 def test_add_source_http_and_key(self, check_call):
73 source = "http://archive.ubuntu.com/ubuntu raring-backports main"
74- key = "akey"
75- fetch.add_source(source=source, key=key)
76- check_call.assert_has_calls([
77- call(['add-apt-repository', '--yes', source]),
78- call(['apt-key', 'adv', '--keyserver', 'hkp://keyserver.ubuntu.com:80',
79- '--recv', key])
80- ])
81-
82- @patch('subprocess.check_call')
83- def test_add_source_https_and_key(self, check_call):
84- source = "http://USER:PASS@private-ppa.launchpad.net/project/awesome"
85- key = "GPGPGP"
86- fetch.add_source(source=source, key=key)
87- check_call.assert_has_calls([
88- call(['add-apt-repository', '--yes', source]),
89- call(['apt-key', 'adv', '--keyserver', 'hkp://keyserver.ubuntu.com:80',
90- '--recv', key])
91- ])
92+ key = '''
93+ -----BEGIN PGP PUBLIC KEY BLOCK-----
94+ [...]
95+ -----END PGP PUBLIC KEY BLOCK-----
96+ '''
97+
98+ received_args = []
99+ received_key = StringIO()
100+ def _check_call(arg, stdin=None):
101+ '''side_effect to store the stdin passed to check_call process.'''
102+ if stdin is not None:
103+ received_args.extend(arg)
104+ received_key.write(stdin.read())
105+
106+ with patch('subprocess.check_call',
107+ side_effect=_check_call) as check_call:
108+ fetch.add_source(source=source, key=key)
109+ check_call.assert_any_call(['add-apt-repository', '--yes', source])
110+ self.assertEqual(['apt-key', 'add', '-'], received_args)
111+ self.assertEqual(key, received_key.getvalue())
112+
113+ def test_add_unparsable_source(self):
114+ source = "propsed" # Minor typo
115+ self.assertRaises(fetch.SourceConfigError, fetch.add_source, source)
116
117 @patch.object(fetch, 'config')
118 @patch.object(fetch, 'add_source')

Subscribers

People subscribed via source and target branches