Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

SSL certificate verification. #791

Merged
merged 59 commits into from
Feb 19, 2013
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
Show all changes
59 commits
Select commit Hold shift + click to select a range
b9ea089
Changing the default index URL to use HTTPS.
Feb 4, 2013
246e974
Added certificate validation against root CA file.
Feb 4, 2013
db79f83
Added CA file to MANIFEST.in
Feb 4, 2013
39d84ab
Adding PEM file to package data.
Feb 4, 2013
d54c695
Adding code to match_hostname.
Feb 4, 2013
7776cfc
added install_requires
Feb 4, 2013
b97969f
'package_dir' not needed
qwcode Feb 6, 2013
6bd3bd4
won't be installing the ssl backport as a dependency
qwcode Feb 6, 2013
11ebe01
warn before installing if no ssl module
qwcode Feb 6, 2013
c612db0
match_hostname from py32
qwcode Feb 6, 2013
73c4614
backwardcompat logic for ssl and match_hostname
qwcode Feb 6, 2013
4bb5ac6
use standard opener when no ssl
qwcode Feb 6, 2013
7e20fd8
move cert_path to locations module
qwcode Feb 6, 2013
b2e0b6d
log relevant message if URLError.reason is SSLError or CertificateError
qwcode Feb 6, 2013
a4a9197
py25 import fixes
qwcode Feb 6, 2013
d50a3b7
license for CA Root Certificates
qwcode Feb 7, 2013
7fac01a
misc test fixes
qwcode Feb 7, 2013
8496406
--cert-path and --no-ssl options
qwcode Feb 7, 2013
226768d
allow py25 tests to work without ssl
qwcode Feb 7, 2013
193562b
more py25 test fixes
qwcode Feb 7, 2013
d8fe463
tun py25 test logic and add assert_raises_regexp
qwcode Feb 8, 2013
456ea80
fix param name in exception message
qwcode Feb 8, 2013
fe17bb4
OpenerDirector for ssl should not contain the default http handler
qwcode Feb 8, 2013
d77380d
ssl cert tests
qwcode Feb 8, 2013
609cfe9
remove duplicate backwardcompat imports and excess logic
qwcode Feb 8, 2013
6dc3212
pypy ssl test fix
qwcode Feb 9, 2013
bb7ba1a
shorter metavar for --cert-path
qwcode Feb 9, 2013
e338436
update authors and changelog
qwcode Feb 9, 2013
857f64e
latest mozilla ca certs aquired securely and generated to pem form
qwcode Feb 9, 2013
49563cf
ssl cert docs updates
qwcode Feb 9, 2013
f92052f
py25 socket patch to work with ssl backport
qwcode Feb 9, 2013
76b5ebc
use the more common phrase 'CA bundle'
qwcode Feb 10, 2013
83d8b37
only show --allow-no-ssl when no ssl
qwcode Feb 10, 2013
912784f
ssl docs fix
qwcode Feb 10, 2013
9cda4d6
--allow-no-ssl test fix
qwcode Feb 10, 2013
8fc02eb
py25 install tests with ssl backport
qwcode Feb 11, 2013
1cf1a7e
refactor pip.backwardcompat from module to package
qwcode Feb 11, 2013
a6a0ee3
update installation docs related to ssl
qwcode Feb 11, 2013
84f8134
remove HTTPHandler explictly
qwcode Feb 11, 2013
f13f879
unable to get ssl backport to install on travis
qwcode Feb 12, 2013
9b57f89
use local packages, not 'mock', which has invalid ssl download link
qwcode Feb 16, 2013
7c8470a
more local test packages
qwcode Feb 16, 2013
d0c3138
merge with develop
qwcode Feb 16, 2013
809a996
clear up connection compatibility logic
qwcode Feb 16, 2013
22bf924
merge with develop
qwcode Feb 16, 2013
b2a17e5
remove old cert from pem file
qwcode Feb 16, 2013
1857ece
fix import syntax
qwcode Feb 16, 2013
c4753b2
add missing sys import
qwcode Feb 18, 2013
26f9c14
move CA license to our license file
qwcode Feb 18, 2013
f72ddaa
our next virtualenv release will be 1.9
qwcode Feb 18, 2013
a2ba2dc
remove yum/apt-get instructions, since they will likely install non-s…
qwcode Feb 18, 2013
e3f1ce8
proper list indents
qwcode Feb 18, 2013
559d77a
from --cert-path to --cert
qwcode Feb 18, 2013
4a4a141
from --allow-no-ssl to --insecure
qwcode Feb 18, 2013
039e1fc
have 'pip search' use https index url
qwcode Feb 18, 2013
2cbc7fa
improve ssl exception text and docs
qwcode Feb 18, 2013
889e1a0
custom NoSSLError exception instead of util function
qwcode Feb 18, 2013
db9c92a
remove extra indents in docs
qwcode Feb 19, 2013
d6bb9a5
add TODO about options passing
qwcode Feb 19, 2013
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions MANIFEST.in
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ include AUTHORS.txt
include LICENSE.txt
include CHANGES.txt
include PROJECT.txt
include pip/cacert.pem
recursive-include docs *.txt
recursive-include docs *.html
recursive-exclude docs/_build *.txt
Expand Down
12 changes: 12 additions & 0 deletions pip/backwardcompat.py
Original file line number Diff line number Diff line change
Expand Up @@ -110,3 +110,15 @@ def home_lib(home):
else:
lib = os.path.join('lib', 'python')
return os.path.join(home, lib)


## py25 has no builtin ssl module
## only >=py32 has ssl.match_hostname and ssl.CertificateError
try:
import ssl
try:
from ssl import match_hostname, CertificateError
except ImportError:
from backwardcompat_ssl import match_hostname, CertificateError
except ImportError:
ssl = None
60 changes: 60 additions & 0 deletions pip/backwardcompat_ssl.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
"""The match_hostname() function from Python 3.2, essential when using SSL."""

import re

__version__ = '3.2a3'

class CertificateError(ValueError):
pass

def _dnsname_to_pat(dn):
pats = []
for frag in dn.split(r'.'):
if frag == '*':
# When '*' is a fragment by itself, it matches a non-empty dotless
# fragment.
pats.append('[^.]+')
else:
# Otherwise, '*' matches any dotless fragment.
frag = re.escape(frag)
pats.append(frag.replace(r'\*', '[^.]*'))
return re.compile(r'\A' + r'\.'.join(pats) + r'\Z', re.IGNORECASE)

def match_hostname(cert, hostname):
"""Verify that *cert* (in decoded format as returned by
SSLSocket.getpeercert()) matches the *hostname*. RFC 2818 rules
are mostly followed, but IP addresses are not accepted for *hostname*.

CertificateError is raised on failure. On success, the function
returns nothing.
"""
if not cert:
raise ValueError("empty or no certificate")
dnsnames = []
san = cert.get('subjectAltName', ())
for key, value in san:
if key == 'DNS':
if _dnsname_to_pat(value).match(hostname):
return
dnsnames.append(value)
if not san:
# The subject is only checked when subjectAltName is empty
for sub in cert.get('subject', ()):
for key, value in sub:
# XXX according to RFC 2818, the most specific Common Name
# must be used.
if key == 'commonName':
if _dnsname_to_pat(value).match(hostname):
return
dnsnames.append(value)
if len(dnsnames) > 1:
raise CertificateError("hostname %r "
"doesn't match either of %s"
% (hostname, ', '.join(map(repr, dnsnames))))
elif len(dnsnames) == 1:
raise CertificateError("hostname %r "
"doesn't match %r"
% (hostname, dnsnames[0]))
else:
raise CertificateError("no appropriate commonName or "
"subjectAltName fields were found")
3,376 changes: 3,376 additions & 0 deletions pip/cacert.pem

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion pip/cmdoptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ def make_option_group(group, parser):
'-i', '--index-url', '--pypi-url',
dest='index_url',
metavar='URL',
default='http://pypi.python.org/simple/',
default='https://pypi.python.org/simple/',
help='Base URL of Python Package Index (default %default).')

extra_index_url = make_option(
Expand Down
2 changes: 2 additions & 0 deletions pip/commands/install.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
from pip.exceptions import InstallationError, CommandError
from pip.backwardcompat import home_lib
from pip.cmdoptions import make_option_group, index_group
from pip.util import warn_if_no_ssl


class InstallCommand(Command):
Expand Down Expand Up @@ -196,6 +197,7 @@ def _build_package_finder(self, options, index_urls):
mirrors=options.mirrors)

def run(self, options, args):
warn_if_no_ssl()
if options.download_dir:
options.no_install = True
options.ignore_installed = True
Expand Down
50 changes: 45 additions & 5 deletions pip/download.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,20 @@
import os
import re
import shutil
import socket
import sys
import tempfile
from pip.backwardcompat import (xmlrpclib, urllib, urllib2,
urlparse, string_types)
from pip.backwardcompat import (xmlrpclib, urllib, urllib2, httplib,
urlparse, string_types, ssl)
if ssl:
from pip.backwardcompat import match_hostname
from pip.exceptions import InstallationError
from pip.util import (splitext, rmtree, format_size, display_path,
backup_dir, ask_path_exists, unpack_file,
create_download_cache_folder, cache_download)
from pip.vcs import vcs
from pip.log import logger

from pip.locations import cert_path

__all__ = ['xmlrpclib_transport', 'get_file_content', 'urlopen',
'is_url', 'url_to_path', 'path_to_url', 'path_to_url2',
Expand Down Expand Up @@ -65,6 +68,32 @@ def get_file_content(url, comes_from=None):
_scheme_re = re.compile(r'^(http|https|file):', re.I)
_url_slash_drive_re = re.compile(r'/*([a-z])\|', re.I)

class VerifiedHTTPSConnection(httplib.HTTPSConnection):
def connect(self):
# overrides the version in httplib so that we do
# certificate verification
sock = socket.create_connection((self.host, self.port), self.timeout)
if self._tunnel_host:
self.sock = sock
self._tunnel()
# wrap the socket using verification with the root
# certs in trusted_root_certs
self.sock = ssl.wrap_socket(sock,
self.key_file,
self.cert_file,
cert_reqs=ssl.CERT_REQUIRED,
ca_certs=cert_path)
match_hostname(self.sock.getpeercert(), self.host)


# wraps https connections with ssl certificate verification
class VerifiedHTTPSHandler(urllib2.HTTPSHandler):
def __init__(self, connection_class = VerifiedHTTPSConnection):
self.specialized_conn_class = connection_class
urllib2.HTTPSHandler.__init__(self)
def https_open(self, req):
return self.do_open(self.specialized_conn_class, req)


class URLOpener(object):
"""
Expand All @@ -83,7 +112,7 @@ def __call__(self, url):
url, username, password = self.extract_credentials(url)
if username is None:
try:
response = urllib2.urlopen(self.get_request(url))
response = self.get_opener().open(url)
except urllib2.HTTPError:
e = sys.exc_info()[1]
if e.code != 401:
Expand Down Expand Up @@ -120,10 +149,21 @@ def get_response(self, url, username=None, password=None):
self.passman.add_password(None, netloc, username, password)
stored_username, stored_password = self.passman.find_user_password(None, netloc)
authhandler = urllib2.HTTPBasicAuthHandler(self.passman)
opener = urllib2.build_opener(authhandler)
opener = self.get_opener(authhandler)
# FIXME: should catch a 401 and offer to let the user reenter credentials
return opener.open(req)

def get_opener(self, *args):
"""
If ssl module is available, will return secure (verified HTTPS) opener
Otherwise, standard.
"""
if ssl:
https_handler = VerifiedHTTPSHandler()
return urllib2.build_opener(https_handler, *args)
else:
return urllib2.build_opener(*args)

def setup(self, proxystr='', prompting=True):
"""
Sets the proxy handler given the option passed on the command
Expand Down
7 changes: 6 additions & 1 deletion pip/index.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,9 @@
from pip.backwardcompat import (WindowsError, BytesIO,
Queue, urlparse,
URLError, HTTPError, u,
product, url2pathname)
product, url2pathname, ssl)
if ssl:
from pip.backwardcompat import CertificateError
from pip.backwardcompat import Empty as QueueEmpty
from pip.download import urlopen, path_to_url2, url_to_path, geturl, Urllib2HeadRequest

Expand Down Expand Up @@ -485,6 +487,9 @@ def get_page(cls, link, req, cache=None, skip_archives=True):
level =1
desc = 'timed out'
elif isinstance(e, URLError):
#ssl/certificate error
if ssl and hasattr(e, 'reason') and (isinstance(e.reason, ssl.SSLError) or isinstance(e.reason, CertificateError)):
desc = 'there was a problem confirming the ssl certificate %s' % e
log_meth = logger.info
if hasattr(e, 'reason') and isinstance(e.reason, socket.timeout):
desc = 'timed out'
Expand Down
1 change: 1 addition & 0 deletions pip/locations.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from pip.backwardcompat import get_python_lib
import pip.exceptions

cert_path = os.path.join(os.path.dirname(__file__), 'cacert.pem')

def running_under_virtualenv():
"""
Expand Down
19 changes: 18 additions & 1 deletion pip/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,10 @@
import zipfile
import tarfile
import subprocess
import textwrap
from pip.exceptions import InstallationError, BadCommand
from pip.backwardcompat import WindowsError, string_types, raw_input, console_to_str, user_site
from pip.backwardcompat import(WindowsError, string_types, raw_input,
console_to_str, user_site, ssl)
from pip.locations import site_packages, running_under_virtualenv, virtualenv_no_global
from pip.log import logger

Expand Down Expand Up @@ -664,3 +666,18 @@ def call_subprocess(cmd, show_stdout=True,
% (command_desc, proc.returncode, cwd))
if stdout is not None:
return ''.join(all_output)


def warn_if_no_ssl():
"""Warn when there's no ssl"""
if not ssl:
logger.warn(textwrap.dedent("""
#############################################################
## WARNING!! ##
## You don't have an importable ssl module. ##
## We can not provide ssl certified downloads from PyPI. ##
## Install this: http://pypi.python.org/pypi/ssl/ ##
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You might want to use "https" in this URL :)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes : )

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As an additional note, I think that we should probably wait for a keypress, or wait either 10 seconds or a keypress, (or exit and ask to be relaunched with a command line option, eg: --exploit-me). I know it's annoying, but there is a remote code execution vulnerability here; if you simply print a warning and get on downloading the file and running its setup.py, it might be too late for the user.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll go with the consensus, but I was concerned about breaking people's automated processes. @jezdez, @pnasrat ?

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If that's an (understandable) concern, a delay of 10 seconds (shortened by a keypress) should probably solve it.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't like --exploit-me as an option just --nossl is probably sufficient. We probably don't want to break scripts, just announce vocally.

For package maintainers we should also have this in the installation docs.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@pnasrat but if they don't use --nossl ?

which option?

  1. just a message with no delay
  2. 10s delay which gives people time to quit or continue
  3. fail

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If they don't explictly disable SSL fail w/ an error telling them how to bypass it.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

## It provides ssl support for older Pythons. ##
#############################################################
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is pretty good already, I fear that people will freak out if they see it for the first time though. Let's explain why we can't ship ssl support and link to our documentation explaining in detail how to install it.

"""))

1 change: 1 addition & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ def find_version(*file_paths):
url='http://www.pip-installer.org',
license='MIT',
packages=['pip', 'pip.commands', 'pip.vcs'],
package_data={'pip': ['*.pem']},
entry_points=dict(console_scripts=['pip=pip:main', 'pip-%s=pip:main' % sys.version[:3]]),
test_suite='nose.collector',
tests_require=tests_require,
Expand Down