/usr/share/pyshared/softwareproperties/ppa.py is in python-software-properties 0.82.7.5.
This file is owned by root:root, with mode 0o644.
The actual contents of the file can be viewed below.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 | # software-properties PPA support
#
# Copyright (c) 2004-2009 Canonical Ltd.
#
# Author: Michael Vogt <mvo@debian.org>
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License as
# published by the Free Software Foundation; either version 2 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
# USA
import apt_pkg
import json
import re
import os
import subprocess
import tempfile
import shutil
from threading import Thread
import pycurl
DEFAULT_KEYSERVER = "hkp://keyserver.ubuntu.com:80/"
# maintained until 2015
LAUNCHPAD_PPA_API = 'https://launchpad.net/api/1.0/~%s/+archive/%s'
# None means use pycurl default
LAUNCHPAD_PPA_CERT = None
def encode(s):
return re.sub("[^a-zA-Z0-9_-]","_", s)
def expand_ppa_line(abrev, distro_codename):
""" Convert an abbreviated ppa name of the form 'ppa:$name' to a
proper sources.list line of the form 'deb ...' """
# leave non-ppa: lines unchanged
if not abrev.startswith("ppa:"):
return (abrev, None)
# FIXME: add support for dependency PPAs too (once we can get them
# via some sort of API, see LP #385129)
abrev = abrev.split(":")[1]
ppa_owner = abrev.split("/")[0]
try:
ppa_name = abrev.split("/")[1]
except IndexError, e:
ppa_name = "ppa"
sourceslistd = apt_pkg.config.find_dir("Dir::Etc::sourceparts")
line = "deb http://ppa.launchpad.net/%s/%s/ubuntu %s main" % (
ppa_owner, ppa_name, distro_codename)
filename = "%s/%s-%s-%s.list" % (
sourceslistd, encode(ppa_owner), encode(ppa_name), distro_codename)
return (line, filename)
class CurlCallback:
def __init__(self):
self.contents = ''
def body_callback(self, buf):
self.contents = self.contents + buf
def get_ppa_info_from_lp(owner_name, ppa_name):
lp_url = LAUNCHPAD_PPA_API % (owner_name, ppa_name)
# we ask for a JSON structure from lp_page, we could use
# simplejson, but the format is simple enough for the regexp
callback = CurlCallback()
curl = pycurl.Curl()
curl.setopt(pycurl.SSL_VERIFYPEER, 1)
curl.setopt(pycurl.SSL_VERIFYHOST, 2)
curl.setopt(pycurl.WRITEFUNCTION, callback.body_callback)
# only useful for testing
if LAUNCHPAD_PPA_CERT:
curl.setopt(pycurl.CAINFO, LAUNCHPAD_PPA_CERT)
curl.setopt(pycurl.URL, str(lp_url))
curl.setopt(pycurl.HTTPHEADER, ["Accept: application/json"])
curl.perform()
curl.close()
lp_page = callback.contents
return json.loads(lp_page)
class AddPPASigningKeyThread(Thread):
" thread class for adding the signing key in the background "
def __init__(self, ppa_path, keyserver=None):
Thread.__init__(self)
self.ppa_path = ppa_path
self.keyserver = (keyserver if keyserver is not None
else DEFAULT_KEYSERVER)
def run(self):
self.add_ppa_signing_key(self.ppa_path)
def add_ppa_signing_key(self, ppa_path):
"""Query and add the corresponding PPA signing key.
The signing key fingerprint is obtained from the Launchpad PPA page,
via a secure channel, so it can be trusted.
"""
def cleanup(tmpdir):
import shutil
shutil.rmtree(tmp_keyring_dir)
def fail_with_msg(msg, tmpdir=None):
print msg
if tmpdir:
cleanup(tmpdir)
return False
owner_name, ppa_name, distro = ppa_path[1:].split('/')
try:
ppa_info = get_ppa_info_from_lp(owner_name, ppa_name)
except pycurl.error as e:
print "Error reading %s: %s" % (ppa_path, e[1])
return False
try:
signing_key_fingerprint = ppa_info["signing_key_fingerprint"]
except IndexError as e:
print "Error: can't find signing_key_fingerprint at %s" % ppa_path
return False
# double check that the signing key is a v4 fingerprint (160bit)
if len(signing_key_fingerprint) < 160/8:
return fail_with_msg(
"Error: signing key fingerprint '%s' too short" %
signing_key_fingerprint)
# create a temp keyring dir
tmp_keyring_dir = tempfile.mkdtemp()
tmp_secret_keyring = os.path.join(tmp_keyring_dir, "secring.gpg")
tmp_keyring = os.path.join(tmp_keyring_dir, "pubring.gpg")
# default options for gpg
gpg_default_options = [
"gpg",
"--no-default-keyring", "--no-options",
"--homedir", tmp_keyring_dir,
]
# download the key to a temp keyring first
res = subprocess.call(gpg_default_options + [
"--secret-keyring", tmp_secret_keyring,
"--keyring", tmp_keyring,
"--keyserver", self.keyserver,
"--recv", signing_key_fingerprint,
])
if res != 0:
return fail_with_msg("recv failed", tmp_keyring_dir)
# now export again using the long key id (to ensure that there is
# really only this one key in our keyring) and not someone MITM us
tmp_export_keyring = os.path.join(tmp_keyring_dir, "export-keyring.gpg")
res = subprocess.call(gpg_default_options + [
"--keyring", tmp_keyring,
"--output", tmp_export_keyring,
"--export", signing_key_fingerprint,
])
if res != 0:
return fail_with_msg("export failed", tmp_keyring_dir)
# now verify the fingerprint, this is probably redundant as we
# exported by the fingerprint in the previous command but its
# still good paranoia
output = subprocess.Popen(
gpg_default_options + [
"--keyring", tmp_export_keyring,
"--fingerprint",
"--batch",
"--with-colons",
],
stdout=subprocess.PIPE,
universal_newlines=True).communicate()[0]
got_fingerprint=None
for line in output.splitlines():
if line.startswith("fpr:"):
got_fingerprint = line.split(":")[9]
# stop after the first to ensure no subkey trickery
break
if got_fingerprint != signing_key_fingerprint:
return fail_with_msg(
"Fingerprints do not match, not importing: '%s' != '%s'" % (
signing_key_fingerprint, got_fingerprint))
res = subprocess.call(["apt-key", "add", tmp_keyring])
# cleanup
cleanup(tmp_keyring_dir)
return (res == 0)
if __name__ == "__main__":
import sys
owner_name, ppa_name = sys.argv[1].split(":")[1].split("/")
print get_ppa_info_from_lp(owner_name, ppa_name)
|