Merge lp:~xnox/software-properties/gnupg-fix-all-the-things into lp:software-properties

Proposed by Dimitri John Ledkov
Status: Merged
Merged at revision: 1013
Proposed branch: lp:~xnox/software-properties/gnupg-fix-all-the-things
Merge into: lp:software-properties
Diff against target: 595 lines (+181/-170)
10 files modified
add-apt-repository (+4/-4)
debian/changelog (+24/-0)
debian/control (+2/-2)
debian/tests/add-apt-repository (+11/-7)
debian/tests/control (+3/-3)
debian/tests/run-tests (+0/-9)
software-properties-gtk (+2/-3)
software-properties-kde (+1/-5)
softwareproperties/ppa.py (+71/-103)
tests/test_lp.py (+63/-34)
To merge this branch: bzr merge lp:~xnox/software-properties/gnupg-fix-all-the-things
Reviewer Review Type Date Requested Status
Ubuntu Core Development Team Pending
Review via email: mp+342500@code.launchpad.net

Commit message

   * ppa.py:
    - rework key retrieval, instead of using hkp & gnupg/dirmngr, use https
      & python's built in urllib.
    - thus, add-apt-key for PPAs observes https_proxy for key retrieval
    - simplify gnupg operations, depend on gpg package only, and use
      import/public key operations only.
    - fix unicode process output bugs, when operating in a non-UTF-8
      locale, thus enabling to import keys for my ppas in C locale.
    - avoid creating trustdb, or requiring any gpg-agent systemd socket to
      be activated
    - update tests to execute key addition fully with less things stubbed
      out with mock
    - stop using apt-key for installing keys
    - deprecate --keyserver option, making HTTPS access to
      keyserver.ubuntu.com required

Description of the change

   * ppa.py:
    - rework key retrieval, instead of using hkp & gnupg/dirmngr, use https
      & python's built in urllib.
    - thus, add-apt-key for PPAs observes https_proxy for key retrieval
    - simplify gnupg operations, depend on gpg package only, and use
      import/public key operations only.
    - fix unicode process output bugs, when operating in a non-UTF-8
      locale, thus enabling to import keys for my ppas in C locale.
    - avoid creating trustdb, or requiring any gpg-agent systemd socket to
      be activated
    - update tests to execute key addition fully with less things stubbed
      out with mock
    - stop using apt-key for installing keys
    - deprecate --keyserver option, making HTTPS access to
      keyserver.ubuntu.com required

Autopkgtests pass, results e.g.:
https://objectstorage.prodstack4-5.canonical.com/v1/AUTH_39a8dbb93caf4ec889f8a1b7f69885db/bileto-3222-excuses/2018-04-02_11:20:01/3222_bionic_excuses.html

To post a comment you must log in.
1019. By Dimitri John Ledkov

 - dirmngr is a heavy dependency and not used, and it is hard to pass
   proxy information to it when invoking gpg from a non-standard homedir
 - LP: #1755192, LP: #1713962, LP: #1699086, LP: #1510220, LP: #1433761,
   LP: #1395321, LP: #1312267

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'add-apt-repository'
--- add-apt-repository 2018-02-08 15:06:39 +0000
+++ add-apt-repository 2018-04-03 08:42:06 +0000
@@ -10,7 +10,6 @@
1010
11from softwareproperties.SoftwareProperties import SoftwareProperties, shortcut_handler11from softwareproperties.SoftwareProperties import SoftwareProperties, shortcut_handler
12from softwareproperties.shortcuts import ShortcutException12from softwareproperties.shortcuts import ShortcutException
13from softwareproperties.ppa import DEFAULT_KEYSERVER
14import aptsources13import aptsources
15from aptsources.sourceslist import SourceEntry14from aptsources.sourceslist import SourceEntry
16from optparse import OptionParser15from optparse import OptionParser
@@ -60,9 +59,6 @@
60 parser.add_option("-r", "--remove", action="store_true",59 parser.add_option("-r", "--remove", action="store_true",
61 dest="remove", default=False,60 dest="remove", default=False,
62 help=_("remove repository from sources.list.d directory"))61 help=_("remove repository from sources.list.d directory"))
63 parser.add_option("-k", "--keyserver",
64 dest="keyserver", default=DEFAULT_KEYSERVER,
65 help=_("URL of keyserver. Default: %default"))
66 parser.add_option("-s", "--enable-source", action="store_true",62 parser.add_option("-s", "--enable-source", action="store_true",
67 dest="enable_source", default=False,63 dest="enable_source", default=False,
68 help=_("Allow downloading of the source packages from the repository"))64 help=_("Allow downloading of the source packages from the repository"))
@@ -75,6 +71,10 @@
75 parser.add_option("-u", "--update", action="store_true",71 parser.add_option("-u", "--update", action="store_true",
76 dest="update", default=True,72 dest="update", default=True,
77 help=_("Update package cache after adding (legacy option)"))73 help=_("Update package cache after adding (legacy option)"))
74 parser.add_option("-k", "--keyserver",
75 dest="keyserver", default="",
76 help=_("Legacy option, unused."))
77
78 (options, args) = parser.parse_args()78 (options, args) = parser.parse_args()
79 79
80 # We prefer to run apt-get update here. The built-in update support80 # We prefer to run apt-get update here. The built-in update support
8181
=== modified file 'debian/changelog'
--- debian/changelog 2018-03-21 16:43:05 +0000
+++ debian/changelog 2018-04-03 08:42:06 +0000
@@ -1,3 +1,27 @@
1software-properties (0.96.24.25) bionic; urgency=medium
2
3 * ppa.py:
4 - rework key retrieval, instead of using hkp & gnupg/dirmngr, use https
5 & python's built in urllib.
6 - thus, add-apt-key for PPAs observes https_proxy for key retrieval
7 - simplify gnupg operations, depend on gpg package only, and use
8 import/public key operations only.
9 - fix unicode process output bugs, when operating in a non-UTF-8
10 locale, thus enabling to import keys for my ppas in C locale.
11 - avoid creating trustdb, or requiring any gpg-agent systemd socket to
12 be activated
13 - update tests to execute key addition fully with less things stubbed
14 out with mock
15 - stop using apt-key for installing keys
16 - dirmngr is a heavy dependency and not used, and it is hard to pass
17 proxy information to it when invoking gpg from a non-standard homedir
18 - deprecate --keyserver option, making HTTPS access to
19 keyserver.ubuntu.com required
20 - LP: #1755192, LP: #1713962, LP: #1699086, LP: #1510220, LP: #1433761,
21 LP: #1395321, LP: #1312267
22
23 -- Dimitri John Ledkov <xnox@ubuntu.com> Mon, 02 Apr 2018 10:19:34 +0100
24
1software-properties (0.96.24.24) bionic; urgency=medium25software-properties (0.96.24.24) bionic; urgency=medium
226
3 * DialogAuth.py: Implement a dialog to choose between an ubuntu sso account or27 * DialogAuth.py: Implement a dialog to choose between an ubuntu sso account or
428
=== modified file 'debian/control'
--- debian/control 2018-03-21 15:00:01 +0000
+++ debian/control 2018-04-03 08:42:06 +0000
@@ -20,7 +20,7 @@
20Section: python20Section: python
21Architecture: all21Architecture: all
22Depends: ${python:Depends}, ${misc:Depends}, python, python-apt (>=22Depends: ${python:Depends}, ${misc:Depends}, python, python-apt (>=
23 0.6.20ubuntu16), python-pycurl, lsb-release, iso-codes, gnupg, dirmngr23 0.6.20ubuntu16), python-pycurl, lsb-release, iso-codes, gpg
24Recommends: unattended-upgrades24Recommends: unattended-upgrades
25Description: manage the repositories that you install software from25Description: manage the repositories that you install software from
26 This software provides an abstraction of the used apt repositories.26 This software provides an abstraction of the used apt repositories.
@@ -31,7 +31,7 @@
31Section: python31Section: python
32Architecture: all32Architecture: all
33Depends: ${python3:Depends}, ${misc:Depends}, python3, python3-apt (>=33Depends: ${python3:Depends}, ${misc:Depends}, python3, python3-apt (>=
34 0.6.20ubuntu16), lsb-release, iso-codes, gnupg, dirmngr34 0.6.20ubuntu16), lsb-release, iso-codes, gpg
35Recommends: unattended-upgrades35Recommends: unattended-upgrades
36Description: manage the repositories that you install software from36Description: manage the repositories that you install software from
37 This software provides an abstraction of the used apt repositories.37 This software provides an abstraction of the used apt repositories.
3838
=== modified file 'debian/tests/add-apt-repository'
--- debian/tests/add-apt-repository 2016-09-01 10:23:19 +0000
+++ debian/tests/add-apt-repository 2018-04-03 08:42:06 +0000
@@ -1,10 +1,14 @@
1#!/bin/sh1#!/bin/sh
2set -e2set -e
33
4rm -f /etc/apt/sources.list.d/xnox-ubuntu-nonvirt-*.list4for locale in C.UTF-8 C
5rm -f /etc/apt/trusted.gpg.d/xnox_ubuntu_nonvirt.gpg5do
66 export LC_ALL=$locale
7add-apt-repository ppa:xnox/nonvirt --yes7 echo LC_ALL=$locale test...
88 rm -f /etc/apt/sources.list.d/xnox-ubuntu-nonvirt-*.list
9[ -s /etc/apt/sources.list.d/xnox-ubuntu-nonvirt-*.list ]9 rm -f /etc/apt/trusted.gpg.d/xnox_ubuntu_nonvirt.gpg
10[ -s /etc/apt/trusted.gpg.d/xnox_ubuntu_nonvirt.gpg ]10 add-apt-repository ppa:xnox/nonvirt --yes --no-update
11 [ -s /etc/apt/sources.list.d/xnox-ubuntu-nonvirt-*.list ]
12 [ -s /etc/apt/trusted.gpg.d/xnox_ubuntu_nonvirt.gpg ]
13 gpg -q --homedir $(mktemp -d) --no-default-keyring --keyring /etc/apt/trusted.gpg.d/xnox_ubuntu_nonvirt.gpg --fingerprint
14done
1115
=== modified file 'debian/tests/control'
--- debian/tests/control 2017-04-10 21:30:25 +0000
+++ debian/tests/control 2018-04-03 08:42:06 +0000
@@ -1,6 +1,6 @@
1Tests: run-tests1Tests: run-tests
2Depends: @, xvfb, dbus-x11, python-dbus, python-pycurl, python3-pycurl, python-mock, python3-mock, python-distutils-extra, python-setuptools, python3-distutils-extra, python3-setuptools, python-gi, python3-gi, pyflakes, pyflakes3, gnupg2Depends: @, xvfb, dbus-x11, python-dbus, python-pycurl, python3-pycurl, python-mock, python3-mock, python-distutils-extra, python-setuptools, python3-distutils-extra, python3-setuptools, python-gi, python3-gi, pyflakes, pyflakes3, gpg
33
4Tests: add-apt-repository4Tests: add-apt-repository
5Depends: software-properties-common, gnupg5Depends: software-properties-common, gpg
6Restrictions: needs-root, breaks-testbed, allow-stderr6Restrictions: needs-root, breaks-testbed
77
=== modified file 'debian/tests/run-tests'
--- debian/tests/run-tests 2017-04-10 21:30:25 +0000
+++ debian/tests/run-tests 2018-04-03 08:42:06 +0000
@@ -13,15 +13,6 @@
13# the test work in other locales too.13# the test work in other locales too.
14export LC_ALL=C.UTF-814export LC_ALL=C.UTF-8
1515
16# dirmngr and/or gpg-agent fail without ~/.gnupg present maybe some
17# places should be properly declaring homedir, instead of just keyrings
18if ! out=$(gpg --list-keys 2>&1); then
19 ret=$?
20 echo "Failed [$ret] to initialize ~/.gnupg with: gpg --list-keys" 1>&2
21 echo "$out" 1>&2;
22 exit $ret
23fi
24
25code=016code=0
26if [ "$TMPDIR" ]; then17if [ "$TMPDIR" ]; then
27 env -u TMPDIR xvfb-run -e "$tmpdir/xvfb.log" sh -ec "TMPDIR=\"$TMPDIR\" python setup.py test && python3 setup.py test" || code="$?"18 env -u TMPDIR xvfb-run -e "$tmpdir/xvfb.log" sh -ec "TMPDIR=\"$TMPDIR\" python setup.py test && python3 setup.py test" || code="$?"
2819
=== modified file 'software-properties-gtk'
--- software-properties-gtk 2013-06-19 07:30:35 +0000
+++ software-properties-gtk 2018-04-03 08:42:06 +0000
@@ -35,7 +35,6 @@
35#sys.path.append("@prefix@/share/update-manager/python")35#sys.path.append("@prefix@/share/update-manager/python")
3636
37from softwareproperties.gtk.SoftwarePropertiesGtk import SoftwarePropertiesGtk37from softwareproperties.gtk.SoftwarePropertiesGtk import SoftwarePropertiesGtk
38from softwareproperties.ppa import DEFAULT_KEYSERVER
3938
40if __name__ == "__main__":39if __name__ == "__main__":
41 _ = gettext.gettext40 _ = gettext.gettext
@@ -68,8 +67,8 @@
68 action="store", type="string", default=None,67 action="store", type="string", default=None,
69 help="Enable PPA with the given name")68 help="Enable PPA with the given name")
70 parser.add_option("-k", "--keyserver",69 parser.add_option("-k", "--keyserver",
71 dest="keyserver", default=DEFAULT_KEYSERVER,70 dest="keyserver", default="",
72 help="URL of keyserver. Default: %default")71 help="Legacy option, unused")
73 parser.add_option("--data-dir", "",72 parser.add_option("--data-dir", "",
74 action="store", type="string", default="/usr/share/software-properties/",73 action="store", type="string", default="/usr/share/software-properties/",
75 help="Use data files (UI) from the given directory")74 help="Use data files (UI) from the given directory")
7675
=== modified file 'software-properties-kde'
--- software-properties-kde 2014-12-01 07:04:29 +0000
+++ software-properties-kde 2018-04-03 08:42:06 +0000
@@ -36,7 +36,6 @@
36#sys.path.append("@prefix@/share/update-manager/python")36#sys.path.append("@prefix@/share/update-manager/python")
3737
38from softwareproperties.kde.SoftwarePropertiesKDE import SoftwarePropertiesKDE38from softwareproperties.kde.SoftwarePropertiesKDE import SoftwarePropertiesKDE
39from softwareproperties.ppa import DEFAULT_KEYSERVER
4039
41import sip40import sip
4241
@@ -45,7 +44,6 @@
45 massive_debug = False44 massive_debug = False
46 no_update = False45 no_update = False
47 enable_component = ""46 enable_component = ""
48 keyserver = DEFAULT_KEYSERVER
4947
50#--------------- main ------------------48#--------------- main ------------------
51if __name__ == '__main__':49if __name__ == '__main__':
@@ -89,7 +87,7 @@
89 "name");87 "name");
90 parser.addOption(ppaOption);88 parser.addOption(ppaOption);
91 keyServerOption = QCommandLineOption("keyserver",89 keyServerOption = QCommandLineOption("keyserver",
92 _("URL of keyserver"),90 _("Legacy option, unused"),
93 "url");91 "url");
94 parser.addOption(keyServerOption);92 parser.addOption(keyServerOption);
95 winidOption = QCommandLineOption("attach", _("Win ID to act as a dialogue for"),93 winidOption = QCommandLineOption("attach", _("Win ID to act as a dialogue for"),
@@ -126,8 +124,6 @@
126 attachWinID = None124 attachWinID = None
127 if parser.isSet(winidOption):125 if parser.isSet(winidOption):
128 attachWinID = parser.value(winidOption)126 attachWinID = parser.value(winidOption)
129 if parser.isSet(keyServerOption):
130 options.keyserver = parser.value(keyServerOption)
131 # datadir has a default value, so always read it127 # datadir has a default value, so always read it
132 data_dir = parser.value(dataDirOption)128 data_dir = parser.value(dataDirOption)
133129
134130
=== modified file 'softwareproperties/ppa.py'
--- softwareproperties/ppa.py 2016-09-21 20:01:50 +0000
+++ softwareproperties/ppa.py 2018-04-03 08:42:06 +0000
@@ -46,7 +46,7 @@
46 HTTPError = pycurl.error46 HTTPError = pycurl.error
4747
4848
49DEFAULT_KEYSERVER = "hkp://keyserver.ubuntu.com:80/"49SKS_KEYSERVER = 'https://keyserver.ubuntu.com/pks/lookup?op=get&options=mr&exact=on&search=0x%s'
50# maintained until 201550# maintained until 2015
51LAUNCHPAD_PPA_API = 'https://launchpad.net/api/1.0/%s/+archive/%s'51LAUNCHPAD_PPA_API = 'https://launchpad.net/api/1.0/%s/+archive/%s'
52LAUNCHPAD_USER_API = 'https://launchpad.net/api/1.0/%s'52LAUNCHPAD_USER_API = 'https://launchpad.net/api/1.0/%s'
@@ -79,13 +79,20 @@
79def encode(s):79def encode(s):
80 return re.sub("[^a-zA-Z0-9_-]", "_", s)80 return re.sub("[^a-zA-Z0-9_-]", "_", s)
8181
82def get_info_from_lp(lp_url):82def get_info_from_https(url, accept_json):
83 if NEED_PYCURL:83 if NEED_PYCURL:
84 # python2 has no cert verification so we need pycurl84 # python2 has no cert verification so we need pycurl
85 return _get_https_content_pycurl(lp_url)85 data = _get_https_content_pycurl(url, accept_json)
86 else:86 else:
87 # python3 has cert verification so we can use the buildin urllib87 # python3 has cert verification so we can use the buildin urllib
88 return _get_https_content_py3(lp_url)88 data = _get_https_content_py3(url, accept_json)
89 if accept_json:
90 return json.loads(data)
91 else:
92 return data
93
94def get_info_from_lp(lp_url):
95 return get_info_from_https(lp_url, True)
8996
90def get_ppa_info_from_lp(owner_name, ppa):97def get_ppa_info_from_lp(owner_name, ppa):
91 if owner_name[0] != '~':98 if owner_name[0] != '~':
@@ -106,19 +113,20 @@
106 return os.path.basename(get_info_from_lp(lp_url)["current_series_link"])113 return os.path.basename(get_info_from_lp(lp_url)["current_series_link"])
107114
108115
109def _get_https_content_py3(lp_url):116def _get_https_content_py3(lp_url, accept_json):
110 try:117 try:
111 request = urllib.request.Request(str(lp_url), headers={"Accept":" application/json"})118 headers = {"Accept":" application/json"} if accept_json else {}
119 request = urllib.request.Request(str(lp_url), headers=headers)
112 lp_page = urllib.request.urlopen(request, cafile=LAUNCHPAD_PPA_CERT)120 lp_page = urllib.request.urlopen(request, cafile=LAUNCHPAD_PPA_CERT)
113 json_data = lp_page.read().decode("utf-8", "strict")121 data = lp_page.read().decode("utf-8", "strict")
114 except (URLError, HTTPException) as e:122 except (URLError, HTTPException) as e:
115 # HTTPException doesn't have a reason but might have a string123 # HTTPException doesn't have a reason but might have a string
116 # representation124 # representation
117 reason = hasattr(e, "reason") and e.reason or e125 reason = hasattr(e, "reason") and e.reason or e
118 raise PPAException("Error reading %s: %s" % (lp_url, reason), e)126 raise PPAException("Error reading %s: %s" % (lp_url, reason), e)
119 return json.loads(json_data)127 return data
120128
121def _get_https_content_pycurl(lp_url):129def _get_https_content_pycurl(lp_url, accept_json):
122 # this is the fallback code for python2130 # this is the fallback code for python2
123 try:131 try:
124 callback = CurlCallback()132 callback = CurlCallback()
@@ -129,16 +137,17 @@
129 if LAUNCHPAD_PPA_CERT:137 if LAUNCHPAD_PPA_CERT:
130 curl.setopt(pycurl.CAINFO, LAUNCHPAD_PPA_CERT)138 curl.setopt(pycurl.CAINFO, LAUNCHPAD_PPA_CERT)
131 curl.setopt(pycurl.URL, str(lp_url))139 curl.setopt(pycurl.URL, str(lp_url))
132 curl.setopt(pycurl.HTTPHEADER, ["Accept: application/json"])140 if accept_json:
141 curl.setopt(pycurl.HTTPHEADER, ["Accept: application/json"])
133 curl.perform()142 curl.perform()
134 response = curl.getinfo(curl.RESPONSE_CODE)143 response = curl.getinfo(curl.RESPONSE_CODE)
135 curl.close()144 curl.close()
136 json_data = callback.contents145 data = callback.contents
137 except pycurl.error as e:146 except pycurl.error as e:
138 raise PPAException("Error reading %s: %s" % (lp_url, e), e)147 raise PPAException("Error reading %s: %s" % (lp_url, e), e)
139 if response != 200:148 if response != 200:
140 raise PPAException("Error reading %s: response code %i" % (lp_url, response))149 raise PPAException("Error reading %s: response code %i" % (lp_url, response))
141 return json.loads(json_data)150 return data
142151
143152
144def mangle_ppa_shortcut(shortcut):153def mangle_ppa_shortcut(shortcut):
@@ -168,14 +177,18 @@
168class AddPPASigningKey(object):177class AddPPASigningKey(object):
169 " thread class for adding the signing key in the background "178 " thread class for adding the signing key in the background "
170179
171 GPG_DEFAULT_OPTIONS = ["gpg", "--no-default-keyring", "--no-options"]
172
173 def __init__(self, ppa_path, keyserver=None):180 def __init__(self, ppa_path, keyserver=None):
174 self.ppa_path = ppa_path181 self.ppa_path = ppa_path
175 self.keyserver = (keyserver if keyserver is not None182 self._homedir = tempfile.mkdtemp()
176 else DEFAULT_KEYSERVER)183
177184 def __del__(self):
178 def _recv_key(self, keyring, secret_keyring, signing_key_fingerprint, keyring_dir):185 shutil.rmtree(self._homedir)
186
187 def gpg_cmd(self, args):
188 cmd = "gpg -q --homedir %s --no-default-keyring --no-options --import --import-options %s" % (self._homedir, args)
189 return subprocess.Popen(cmd.split(), stdin=subprocess.PIPE, stdout=subprocess.PIPE)
190
191 def _recv_key(self, signing_key_fingerprint):
179 try:192 try:
180 # double check that the signing key is a v4 fingerprint (160bit)193 # double check that the signing key is a v4 fingerprint (160bit)
181 if not verify_keyid_is_v4(signing_key_fingerprint):194 if not verify_keyid_is_v4(signing_key_fingerprint):
@@ -185,57 +198,30 @@
185 except TypeError:198 except TypeError:
186 print("Error: signing key fingerprint does not exist")199 print("Error: signing key fingerprint does not exist")
187 return False200 return False
188 # then get it201
189 res = subprocess.call(self.GPG_DEFAULT_OPTIONS + [202 return get_info_from_https(SKS_KEYSERVER % signing_key_fingerprint, accept_json=False)
190 "--homedir", keyring_dir,203
191 "--secret-keyring", secret_keyring,204 def _minimize_key(self, key):
192 "--keyring", keyring,205 p = self.gpg_cmd("import-minimal,import-export")
193 "--keyserver", self.keyserver,206 (minimal_key, _) = p.communicate(key.encode())
194 "--recv", signing_key_fingerprint,207
195 ])208 if p.returncode != 0:
196 return (res == 0)209 return False
197210 return minimal_key
198 def _export_key(self, keyring, export_keyring, signing_key_fingerprint, keyring_dir):211
199 res = subprocess.call(self.GPG_DEFAULT_OPTIONS + [212 def _get_fingerprints(self, key):
200 "--homedir", keyring_dir,
201 "--keyring", keyring,
202 "--output", export_keyring,
203 "--export", signing_key_fingerprint,
204 ])
205 if res != 0:
206 return False
207 return True
208
209 def _export_armor_key(self, keyring, export_keyring, signing_key_fingerprint, keyring_dir):
210 res = subprocess.call(self.GPG_DEFAULT_OPTIONS + [
211 "--homedir", keyring_dir,
212 "--keyring", keyring,
213 "--output", export_keyring,
214 "--armor",
215 "--export", signing_key_fingerprint,
216 ])
217 if res != 0:
218 return False
219 return True
220
221 def _get_fingerprints(self, keyring, keyring_dir):
222 cmd = self.GPG_DEFAULT_OPTIONS + [
223 "--homedir", keyring_dir,
224 "--keyring", keyring,
225 "--fingerprint",
226 "--batch",
227 "--with-colons",
228 ]
229 output = subprocess.check_output(cmd, universal_newlines=True)
230 fingerprints = []213 fingerprints = []
231 for line in output.splitlines():214 p = self.gpg_cmd("show-only --fingerprint --batch --with-colons")
232 if line.startswith("fpr:"):215 (output, _) = p.communicate(key)
233 fingerprints.append(line.split(":")[9])216 if p.returncode == 0:
217 for line in output.decode('utf-8').splitlines():
218 if line.startswith("fpr:"):
219 fingerprints.append(line.split(":")[9])
234 return fingerprints220 return fingerprints
235221
236 def _verify_fingerprint(self, keyring, expected_fingerprint, keyring_dir):222 def _verify_fingerprint(self, key, expected_fingerprint):
237 got_fingerprints = self._get_fingerprints(keyring, keyring_dir)223 got_fingerprints = self._get_fingerprints(key)
238 if len(got_fingerprints) > 1:224 if len(got_fingerprints) != 1:
239 print("Got '%s' fingerprints, expected only one" %225 print("Got '%s' fingerprints, expected only one" %
240 len(got_fingerprints))226 len(got_fingerprints))
241 return False227 return False
@@ -255,9 +241,6 @@
255 if ppa_path is None:241 if ppa_path is None:
256 ppa_path = self.ppa_path242 ppa_path = self.ppa_path
257243
258 def cleanup(tmpdir):
259 shutil.rmtree(tmp_keyring_dir)
260
261 try:244 try:
262 ppa_info = get_ppa_info(ppa_path)245 ppa_info = get_ppa_info(ppa_path)
263 except PPAException as e:246 except PPAException as e:
@@ -268,41 +251,26 @@
268 except IndexError as e:251 except IndexError as e:
269 print("Error: can't find signing_key_fingerprint at %s" % ppa_path)252 print("Error: can't find signing_key_fingerprint at %s" % ppa_path)
270 return False253 return False
271 # create temp keyrings254
272 tmp_keyring_dir = tempfile.mkdtemp()255 # download the armored_key
273 tmp_secret_keyring = os.path.join(tmp_keyring_dir, "secring.gpg")256 armored_key = self._recv_key(signing_key_fingerprint)
274 tmp_keyring = os.path.join(tmp_keyring_dir, "pubring.gpg")257 if not armored_key:
275 # download the key into a temp keyring first258 return False
276 if not self._recv_key(259
277 tmp_keyring, tmp_secret_keyring, signing_key_fingerprint, tmp_keyring_dir):
278 cleanup(tmp_keyring_dir)
279 return False
280 # now export the key into a temp keyring using the long key id
281 tmp_export_keyring = os.path.join(tmp_keyring_dir, "export-keyring.gpg")
282 if not self._export_key(
283 tmp_keyring, tmp_export_keyring, signing_key_fingerprint, tmp_keyring_dir):
284 cleanup(tmp_keyring_dir)
285 return False
286 # now verify the fingerprint
287 if not self._verify_fingerprint(
288 tmp_export_keyring, signing_key_fingerprint, tmp_keyring_dir):
289 cleanup(tmp_keyring_dir)
290 return False
291 # now export armor key, because apt-key cannot import keybox
292 tmp_export_armor_keyring = os.path.join(tmp_keyring_dir, "export-armor-keyring.gpg")
293 if not self._export_armor_key(
294 tmp_keyring, tmp_export_armor_keyring, signing_key_fingerprint, tmp_keyring_dir):
295 cleanup(tmp_keyring_dir)
296 return False
297 # and add it
298 trustedgpgd = apt_pkg.config.find_dir("Dir::Etc::trustedparts")260 trustedgpgd = apt_pkg.config.find_dir("Dir::Etc::trustedparts")
299 apt_keyring = os.path.join(trustedgpgd, "%s.gpg" % (261 apt_keyring = os.path.join(trustedgpgd, encode(ppa_info["reference"][1:]))
300 encode(ppa_info["reference"][1:])))262
301 res = subprocess.call(["apt-key", "--keyring", apt_keyring, "add",263 minimal_key = self._minimize_key(armored_key)
302 tmp_export_armor_keyring])264 if not minimal_key:
303 # cleanup265 return False
304 cleanup(tmp_keyring_dir)266
305 return (res == 0)267 if not self._verify_fingerprint(minimal_key, signing_key_fingerprint):
268 return False
269
270 with open('%s.gpg' % apt_keyring, 'wb') as f:
271 f.write(minimal_key)
272
273 return True
306274
307275
308class AddPPASigningKeyThread(Thread, AddPPASigningKey):276class AddPPASigningKeyThread(Thread, AddPPASigningKey):
309277
=== modified file 'tests/test_lp.py'
--- tests/test_lp.py 2017-12-20 20:55:27 +0000
+++ tests/test_lp.py 2018-04-03 08:42:06 +0000
@@ -27,6 +27,37 @@
27 'reference': '~mvo/ubuntu/ppa',27 'reference': '~mvo/ubuntu/ppa',
28 }28 }
2929
30MOCK_KEY="""
31-----BEGIN PGP PUBLIC KEY BLOCK-----
32Version: SKS 1.1.6
33Comment: Hostname: keyserver.ubuntu.com
34
35mI0ESXP67wEEAN2m3xWkAP0p1erHbJx1wYBCL6tLqWXESx1BmF0htLzdD9lfsUYiNs+Zgg3w
36uU0PrQIcqZtyTESh514tw3KQ+OAK2I0a2XJR99lXPksiKoxaOOsr0pTVWDYuIlfV3yfmXvnK
37FZSmaMjjKuqQbCwZe8Ev7yry9Gh9pM5Y87MbNT05ABEBAAG0HkxhdW5jaHBhZCBQUEEgZm9y
38IE1pY2hhZWwgVm9ndIi2BBMBAgAgBQJJc/rvAhsDBgsJCAcDAgQVAggDBBYCAwECHgECF4AA
39CgkQEpGWRw6xLwVofAP/YyU3YykXbr8p7wRp1EpFlDmtbPlFXp00gt4Cqlu2AWVOkwkVoMRQ
40Ncb7wog2Z6u7KyUhD8pgC2FEL0+FQjyNemv7D0OYBG+6DLdjtRsv0CumLdWFmviU96j3OcwT
41G2GkIC/eB2maTrV/vj7vlZ0Qe/T1NL6XLpr0A6Rg6JAtkFM=
42=SMbJ
43-----END PGP PUBLIC KEY BLOCK-----
44"""
45
46MOCK_SECOND_KEY="""
47-----BEGIN PGP PUBLIC KEY BLOCK-----
48Version: SKS 1.1.6
49Comment: Hostname: keyserver.ubuntu.com
50
51mI0ESX34EgEEAOTzplZO3TXmb9dRLu7kOuIEia21e4gwQ/RQe+LD7HdhikcETjf2Ruu0mn6S
52sgPLL+duhKxmv6ZciLUgkk0qEDCZuR6BPxdgAIwqmQmFipcv6UTMQitRPUa9WlPU37Qg+joL
53cTBUdamnVq+yJhLmnuO44UWAty85nNJzDd29gxqXABEBAAG0LUxhdW5jaHBhZCBQUEEgZm9y
54INCU0LzQuNGC0YDQuNC5INCb0LXQtNC60L7Qsoi2BBMBAgAgBQJJffgSAhsDBgsJCAcDAgQV
55AggDBBYCAwECHgECF4AACgkQFXlR/kAx0oeuSwQAuhhgWgeeG3F9XMYDqgJShzMSeQOLMKBq
566mNFEL1sDhRdbinf7rwuQFXDSSNCj8/PLa3DF/u09tAm6CTi10iwxxbXf16pTq21gxCA3/xS
57fszv352yZpcN85MD5aozqv7qUCGOQ9Gey7JzgD7L4wMEjyRScVjx1chfLgyapdj822E=
58=pdql
59-----END PGP PUBLIC KEY BLOCK-----
60"""
3061
31class LaunchpadPPATestCase(unittest.TestCase):62class LaunchpadPPATestCase(unittest.TestCase):
32 63
@@ -34,7 +65,7 @@
34 def setUpClass(cls):65 def setUpClass(cls):
35 for k in apt_pkg.config.keys():66 for k in apt_pkg.config.keys():
36 apt_pkg.config.clear(k)67 apt_pkg.config.clear(k)
37 apt_pkg.init()68 apt_pkg.init()
3869
39 @unittest.skipUnless(70 @unittest.skipUnless(
40 "TEST_ONLINE" in os.environ,71 "TEST_ONLINE" in os.environ,
@@ -73,7 +104,13 @@
73 for k in apt_pkg.config.keys():104 for k in apt_pkg.config.keys():
74 apt_pkg.config.clear(k)105 apt_pkg.config.clear(k)
75 apt_pkg.init()106 apt_pkg.init()
76 107 cls.trustedgpg = os.path.join(
108 os.path.dirname(__file__), "aptroot", "etc", "apt", "trusted.gpg.d")
109 try:
110 os.makedirs(cls.trustedgpg)
111 except:
112 pass
113
77 def setUp(self):114 def setUp(self):
78 self.t = AddPPASigningKeyThread("~mvo/ubuntu/ppa")115 self.t = AddPPASigningKeyThread("~mvo/ubuntu/ppa")
79116
@@ -91,38 +128,30 @@
91 self.assertFalse(mock_subprocess.call.called)128 self.assertFalse(mock_subprocess.call.called)
92129
93 @patch("softwareproperties.ppa.get_ppa_info_from_lp")130 @patch("softwareproperties.ppa.get_ppa_info_from_lp")
94 @patch("softwareproperties.ppa.subprocess")131 @patch("softwareproperties.ppa.get_info_from_https")
95 def test_add_ppa_signing_key_wrong_fingerprint(self, mock_subprocess, mock_get_ppa_info):132 def test_add_ppa_signing_key_wrong_fingerprint(self, mock_https, mock_get_ppa_info):
96 mock_get_ppa_info.return_value = MOCK_PPA_INFO133 mock_get_ppa_info.return_value = MOCK_PPA_INFO
97 mock_subprocess.call.return_value = 0134 mock_https.return_value = MOCK_SECOND_KEY
98 with patch.object(self.t, "_get_fingerprints") as mock:135 res = self.t.add_ppa_signing_key("~mvo/ubuntu/ppa")
99 # setup wrong fingerprint136 self.assertFalse(res)
100 mock.return_value = ["48B913EC7093C0C675DADCC04BEF262422DF4087"]137
101 res = self.t.add_ppa_signing_key("~mvo/ubuntu/ppa")138 @patch("softwareproperties.ppa.get_ppa_info_from_lp")
102 self.assertFalse(res)139 @patch("softwareproperties.ppa.get_info_from_https")
103140 def test_add_ppa_signing_key_multiple_fingerprints(self, mock_https, mock_get_ppa_info):
104 @patch("softwareproperties.ppa.get_ppa_info_from_lp")141 mock_get_ppa_info.return_value = MOCK_PPA_INFO
105 @patch("softwareproperties.ppa.subprocess")142 mock_https.return_value = '\n'.join([MOCK_KEY, MOCK_SECOND_KEY])
106 def test_add_ppa_signing_key_multiple_fingerprints(self, mock_subprocess, mock_get_ppa_info):143 res = self.t.add_ppa_signing_key("~mvo/ubuntu/ppa")
107 mock_get_ppa_info.return_value = MOCK_PPA_INFO144 self.assertFalse(res)
108 mock_subprocess.call.return_value = 0145
109 with patch.object(self.t, "_get_fingerprints") as mock:146 @patch("softwareproperties.ppa.get_ppa_info_from_lp")
110 # setup multiple fingerprints147 @patch("softwareproperties.ppa.get_info_from_https")
111 mock.return_value = ["019A25FED88F961763935D7F129196470EB12F05",148 @patch("apt_pkg.config")
112 "48B913EC7093C0C675DADCC04BEF262422DF4087"]149 def test_add_ppa_signing_key_ok(self, mock_config, mock_https, mock_get_ppa_info):
113 res = self.t.add_ppa_signing_key("~mvo/ubuntu/ppa")150 mock_get_ppa_info.return_value = MOCK_PPA_INFO
114 self.assertFalse(res)151 mock_https.return_value = MOCK_KEY
115152 mock_config.find_dir.return_value = self.trustedgpg
116 @patch("softwareproperties.ppa.get_ppa_info_from_lp")153 res = self.t.add_ppa_signing_key("~mvo/ubuntu/ppa")
117 @patch("softwareproperties.ppa.subprocess")154 self.assertTrue(res)
118 def test_add_ppa_signing_key_ok(self, mock_subprocess, mock_get_ppa_info):
119 mock_get_ppa_info.return_value = MOCK_PPA_INFO
120 mock_subprocess.call.return_value = 0
121 # setup correct signing key
122 with patch.object(self.t, "_get_fingerprints") as mock:
123 mock.return_value = ["019A25FED88F961763935D7F129196470EB12F05"]
124 res = self.t.add_ppa_signing_key("~mvo/ubuntu/ppa")
125 self.assertTrue(res)
126 155
127 def test_verify_keyid_is_v4(self):156 def test_verify_keyid_is_v4(self):
128 keyid = "0EB12F05"157 keyid = "0EB12F05"

Subscribers

People subscribed via source and target branches

to status/vote changes: