Skip to content

Commit

Permalink
Verify SSL certificate before any request
Browse files Browse the repository at this point in the history
Use a list of root certificates from Mozilla, which is licensed under
GLv2, too. In any case, [1] allows us to use the file even under MPL.

[1]: http://www.mozilla.org/MPL/2.0/combining-mpl-and-gpl.html

Fixes CVE-2013-2073.
  • Loading branch information
Apostolos Bessas committed May 14, 2013
1 parent fe0bff8 commit e24ea95
Show file tree
Hide file tree
Showing 8 changed files with 4,022 additions and 7 deletions.
4 changes: 2 additions & 2 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
long_description = long_description.decode('utf-8')

package_data = {
'': ['LICENSE', 'README.rst'],
'': ['LICENSE', 'README.rst', 'txclib/cacert.pem'],
}

scripts = ['tx']
Expand Down Expand Up @@ -59,7 +59,7 @@
],
test_suite="tests",
zip_safe=False,
packages=['txclib', ],
packages=['txclib', 'txclib.packages', 'txclib.packages.ssl_match_hostname'],
include_package_data=True,
package_data = package_data,
keywords = ('translation', 'localization', 'internationalization',),
Expand Down
11 changes: 10 additions & 1 deletion tx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@
from optparse import OptionParser, OptionValueError
import os
import sys

import ssl
import errno
from txclib import utils
from txclib import get_version
from txclib.log import set_log_level, logger
Expand Down Expand Up @@ -90,6 +91,14 @@ def main(argv):
cmd = args[0]
try:
utils.exec_command(cmd, args[1:], path_to_tx)
except ssl.SSLError as e:
if 'certificate verify failed' in e.strerror:
logger.error(
'Error: Could not verify the SSL certificate of the remote host'
)
else:
logger.error(errno.errorcode[e.errno])
sys.exit(1)
except utils.UnknownCommandError:
logger.error("tx: Command %s not found" % cmd)
except SystemExit:
Expand Down
3,895 changes: 3,895 additions & 0 deletions txclib/cacert.pem

Large diffs are not rendered by default.

Empty file added txclib/packages/__init__.py
Empty file.
66 changes: 66 additions & 0 deletions txclib/packages/ssl_match_hostname/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
"""The match_hostname() function from Python 3.2, essential when using SSL."""

# See https://bitbucket.org/brandon/backports.ssl_match_hostname

import re

__version__ = '3.2.2'


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 dnsnames:
# The subject is only checked when there is no dNSName entry
# in subjectAltName
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")
7 changes: 5 additions & 2 deletions txclib/project.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,13 @@
# -*- coding: utf-8 -*-

import base64
import copy
import getpass
import os
import re
import fnmatch
import urllib2
import datetime, time
import datetime
import time
import ConfigParser
from txclib.web import *
from txclib.utils import *
Expand Down Expand Up @@ -365,6 +365,7 @@ def pull(self, languages=[], resources=[], overwrite=True, fetchall=False,
sfile = native_path(sfile)
lang_map = self.get_resource_lang_mapping(resource)
host = self.get_resource_host(resource)
verify_ssl(host)
logger.debug("Language mapping is: %s" % lang_map)
if mode is None:
mode = self._get_option(resource, 'mode')
Expand Down Expand Up @@ -522,6 +523,7 @@ def push(self, source=False, translations=False, force=False, resources=[], lang
sfile = native_path(sfile)
lang_map = self.get_resource_lang_mapping(resource)
host = self.get_resource_host(resource)
verify_ssl(host)
logger.debug("Language mapping is: %s" % lang_map)
logger.debug("Using host %s" % host)
self.url_info = {
Expand Down Expand Up @@ -650,6 +652,7 @@ def delete(self, resources=[], languages=[], skip=False, force=False):
for resource in resource_list:
project_slug, resource_slug = resource.split('.')
host = self.get_resource_host(resource)
verify_ssl(host)
self.url_info = {
'host': host,
'project': project_slug,
Expand Down
2 changes: 2 additions & 0 deletions txclib/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from txclib.log import logger
from txclib.exceptions import UnknownCommandError
from txclib.paths import posix_path, native_path, posix_sep
from txclib.web import verify_ssl


def find_dot_tx(path = os.path.curdir, previous = None):
Expand Down Expand Up @@ -73,6 +74,7 @@ def get_details(api_call, username, password, *args, **kwargs):
"""
import base64
url = (API_URLS[api_call] % (kwargs)).encode('UTF-8')
verify_ssl(url)

req = urllib2.Request(url=url)
base64string = base64.encodestring('%s:%s' % (username, password))[:-1]
Expand Down
44 changes: 42 additions & 2 deletions txclib/web.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,16 @@
# -*- coding: utf-8 -*-

import os
import urllib2
import itertools, mimetools, mimetypes
import socket
import ssl
import urlparse
import mimetools
import mimetypes
import platform
from txclib import get_version
from txclib.packages.ssl_match_hostname import match_hostname


# Helper class to enable urllib2 to handle PUT/DELETE requests as well
class RequestWithMethod(urllib2.Request):
Expand All @@ -19,7 +27,7 @@ def get_method(self):


import urllib
import os, stat
import stat
from cStringIO import StringIO


Expand Down Expand Up @@ -96,3 +104,35 @@ def user_agent_identifier():
"""Return the user agent for the client."""
client_info = (get_version(), platform.system(), platform.machine())
return "txclient/%s (%s %s)" % client_info


def _verify_ssl(hostname, port=443):
"""Verify the SSL certificate of the given host."""
sock = socket.create_connection((hostname, port))
try:
ssl_sock = ssl.wrap_socket(
sock, cert_reqs=ssl.CERT_REQUIRED, ca_certs=certs_file()
)
match_hostname(ssl_sock.getpeercert(), hostname)
finally:
sock.close()


def certs_file():
return os.path.join(os.path.dirname(__file__), 'cacert.pem')


def verify_ssl(host):
parts = urlparse.urlparse(host)
if parts.scheme != 'https':
return

if ':' in parts.netloc:
hostname, port = parts.netloc.split(':')
else:
hostname = parts.netloc
if parts.port is not None:
port = parts.port
else:
port = 443
_verify_ssl(hostname, port)

0 comments on commit e24ea95

Please sign in to comment.