2004-07-07 18:15:17 +00:00
|
|
|
# -*- coding: iso-8859-1 -*-
|
2012-06-18 21:05:44 +00:00
|
|
|
# Copyright (C) 2000-2012 Bastian Kleineidam
|
2004-07-07 18:15:17 +00:00
|
|
|
#
|
|
|
|
|
# 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.
|
|
|
|
|
#
|
2009-07-24 21:58:20 +00:00
|
|
|
# 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.,
|
|
|
|
|
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
2005-01-19 15:08:02 +00:00
|
|
|
"""
|
|
|
|
|
Handle https links.
|
|
|
|
|
"""
|
2004-07-07 18:15:17 +00:00
|
|
|
|
2008-05-09 06:16:03 +00:00
|
|
|
from . import httpurl
|
2012-06-18 21:05:44 +00:00
|
|
|
from .const import WARN_HTTPS_CERTIFICATE
|
|
|
|
|
|
2004-07-07 18:15:17 +00:00
|
|
|
|
2004-08-16 19:20:53 +00:00
|
|
|
class HttpsUrl (httpurl.HttpUrl):
|
2005-01-19 01:04:38 +00:00
|
|
|
"""
|
|
|
|
|
Url link with https scheme.
|
|
|
|
|
"""
|
2004-07-07 18:15:17 +00:00
|
|
|
|
2004-08-16 19:20:53 +00:00
|
|
|
def local_check (self):
|
2005-01-19 01:04:38 +00:00
|
|
|
"""
|
|
|
|
|
Check connection if SSL is supported, else ignore.
|
|
|
|
|
"""
|
2004-08-16 19:20:53 +00:00
|
|
|
if httpurl.supportHttps:
|
|
|
|
|
super(HttpsUrl, self).local_check()
|
2004-07-07 18:15:17 +00:00
|
|
|
else:
|
2005-05-06 14:59:22 +00:00
|
|
|
self.add_info(_("%s URL ignored.") % self.scheme.capitalize())
|
2012-06-18 21:05:44 +00:00
|
|
|
|
|
|
|
|
def get_http_object (self, host, scheme):
|
|
|
|
|
"""Open a HTTP connection and check the SSL certificate."""
|
|
|
|
|
h = super(HttpsUrl, self).get_http_object(host, scheme)
|
|
|
|
|
self.check_ssl_certificate(h.sock, host)
|
|
|
|
|
return h
|
|
|
|
|
|
|
|
|
|
def check_ssl_certificate(self, ssl_sock, host):
|
|
|
|
|
"""Run all SSl certificate checks that have not yet been done.
|
|
|
|
|
OpenSSL already checked the SSL notBefore and notAfter dates.
|
|
|
|
|
"""
|
|
|
|
|
cert = ssl_sock.getpeercert()
|
|
|
|
|
if not cert:
|
|
|
|
|
msg = _('empty or no certificate found')
|
|
|
|
|
self.add_ssl_warning(ssl_sock, msg)
|
|
|
|
|
return
|
|
|
|
|
if 'subject' in cert:
|
|
|
|
|
self.check_ssl_hostname(ssl_sock, cert, host)
|
|
|
|
|
else:
|
|
|
|
|
msg = _('certificate did not include "subject" information')
|
|
|
|
|
self.add_ssl_warning(ssl_sock, msg)
|
|
|
|
|
|
|
|
|
|
def check_ssl_hostname(self, ssl_sock, cert, host):
|
|
|
|
|
"""Check the hostname against the certificate according to
|
|
|
|
|
RFC2818.
|
|
|
|
|
"""
|
|
|
|
|
try:
|
|
|
|
|
match_hostname(cert, host)
|
|
|
|
|
except CertificateError, msg:
|
|
|
|
|
self.add_ssl_warning(ssl_sock, msg)
|
|
|
|
|
|
|
|
|
|
def add_ssl_warning(self, ssl_sock, msg):
|
|
|
|
|
"""Add a warning message about an SSL certificate error."""
|
|
|
|
|
cipher_name, ssl_protocol, secret_bits = ssl_sock.cipher()
|
|
|
|
|
err = _(u"SSL warning: %(msg)s. Cipher %(cipher)s, %(protocol)s %s")
|
|
|
|
|
attrs = dict(msg=msg, cipher=cipher_name, protocol=ssl_protocol)
|
|
|
|
|
self.add_warning(err % attrs, tag=WARN_HTTPS_CERTIFICATE)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# Copied from ssl.py in Python 3:
|
|
|
|
|
# Wrapper module for _ssl, providing some additional facilities
|
|
|
|
|
# implemented in Python. Written by Bill Janssen.
|
|
|
|
|
import re
|
|
|
|
|
|
|
|
|
|
class CertificateError(ValueError):
|
|
|
|
|
"""Raised on certificate errors."""
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _dnsname_to_pat(dn):
|
|
|
|
|
"""Convert a DNS certificate name to a hostname matcher."""
|
|
|
|
|
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")
|