Skip to content

Commit

Permalink
Merge branch 'fix/11127-recaptcha-ssl_10809r1_r1' into develop
Browse files Browse the repository at this point in the history
  • Loading branch information
isislovecruft committed Mar 15, 2014
2 parents 35effa8 + 84dd0f3 commit 8d927c1
Show file tree
Hide file tree
Showing 6 changed files with 683 additions and 5 deletions.
7 changes: 4 additions & 3 deletions lib/bridgedb/HTTPServer.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,14 @@
import bridgedb.I18n as I18n
import bridgedb.Util as Util

from recaptcha.client import captcha as recaptcha
from bridgedb import captcha
from bridgedb import crypto
from bridgedb.Filters import filterBridgesByIP6, filterBridgesByIP4
from bridgedb.Filters import filterBridgesByTransport
from bridgedb.Filters import filterBridgesByNotBlockedIn
from bridgedb.parse import headers
from bridgedb import txrecaptcha

from ipaddr import IPv4Address, IPv6Address
from random import randint
import mako.exceptions
Expand Down Expand Up @@ -423,8 +424,8 @@ def checkSolution(self, request):
challenge, response = self.extractClientSolution(request)
clientIP = self.getClientIP(request)
remoteIP = self.getRemoteIP()
solution = recaptcha.submit(challenge, response,
self.recaptchaPrivKey, remoteIP)
solution = txrecaptcha.submit(challenge, response,
self.recaptchaPrivKey, remoteIP)
logging.debug("Captcha from %r. Parameters: %r"
% (Util.logSafely(clientIP), request.args))

Expand Down
3 changes: 2 additions & 1 deletion lib/bridgedb/captcha.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,10 +41,11 @@
import urllib2

from BeautifulSoup import BeautifulSoup
from recaptcha.client.captcha import API_SSL_SERVER

from zope.interface import Interface, Attribute, implements

from bridgedb import crypto
from bridgedb.txrecaptcha import API_SSL_SERVER


class ReCaptchaKeyError(Exception):
Expand Down
98 changes: 97 additions & 1 deletion lib/bridgedb/crypto.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,12 +33,16 @@
import hmac
import logging
import os
import re
import urllib

import OpenSSL.rand
import OpenSSL

from Crypto.Cipher import PKCS1_OAEP
from Crypto.PublicKey import RSA

from twisted.internet import ssl


#: The hash digest to use for HMACs.
DIGESTMOD = hashlib.sha1
Expand Down Expand Up @@ -189,3 +193,95 @@ def hmac_fn(value):
else:
return h_tmp.digest()
return hmac_fn


class SSLVerifyingContextFactory(ssl.CertificateOptions):
"""``OpenSSL.SSL.Context`` factory which does full certificate-chain and
hostname verfication.
"""
isClient = True

def __init__(self, url, **kwargs):
"""Create a client-side verifying SSL Context factory.
To pass acceptable certificates for a server which does
client-authentication checks: initialise with a ``caCerts=[]`` keyword
argument, which should be a list of ``OpenSSL.crypto.X509`` instances
(one for each peer certificate to add to the store), and set
``SSLVerifyingContextFactory.isClient=False``.
:param str url: The URL being requested by an
:api:`twisted.web.client.Agent`.
:param bool isClient: True if we're being used in a client
implementation; False if we're a server.
"""
self.hostname = self.getHostnameFromURL(url)

# ``verify`` here refers to server-side verification of certificates
# presented by a client:
self.verify = False if self.isClient else True
super(SSLVerifyingContextFactory, self).__init__(verify=self.verify,
fixBrokenPeers=True,
**kwargs)

def getContext(self, hostname=None, port=None):
"""Retrieve a configured ``OpenSSL.SSL.Context``.
Any certificates in the ``caCerts`` list given during initialisation
are added to the ``Context``'s certificate store.
The **hostname** and **port** arguments seem unused, but they are
required due to some Twisted and pyOpenSSL internals. See
:api:`twisted.web.client.Agent._wrapContextFactory`.
:rtype: ``OpenSSL.SSL.Context``
:returns: An SSL Context which verifies certificates.
"""
ctx = super(SSLVerifyingContextFactory, self).getContext()
store = ctx.get_cert_store()
verifyOptions = OpenSSL.SSL.VERIFY_PEER
ctx.set_verify(verifyOptions, self.verifyHostname)
return ctx

def getHostnameFromURL(self, url):
"""Parse the hostname from the originally requested URL.
:param str url: The URL being requested by an
:api:`twisted.web.client.Agent`.
:rtype: str
:returns: The full hostname (including any subdomains).
"""
hostname = urllib.splithost(urllib.splittype(url)[1])[0]
logging.debug("Parsed hostname %r for cert CN matching." % hostname)
return hostname

def verifyHostname(self, connection, x509, errnum, depth, okay):
"""Callback method for additional SSL certificate validation.
If the certificate is signed by a valid CA, and the chain is valid,
verify that the level 0 certificate has a subject common name which is
valid for the hostname of the originally requested URL.
:param connection: An ``OpenSSL.SSL.Connection``.
:param x509: An ``OpenSSL.crypto.X509`` object.
:param errnum: A pyOpenSSL error number. See that project's docs.
:param depth: The depth which the current certificate is at in the
certificate chain.
:param bool okay: True if all the pyOpenSSL default checks on the
certificate passed. False otherwise.
"""
commonName = x509.get_subject().commonName
logging.debug("Received cert at level %d: '%s'" % (depth, commonName))

# We only want to verify that the hostname matches for the level 0
# certificate:
if okay and (depth == 0):
cn = commonName.replace('*', '.*')
hostnamesMatch = re.search(cn, self.hostname)
if not hostnamesMatch:
logging.warn("Invalid certificate subject CN for '%s': '%s'"
% (self.hostname, commonName))
return False
logging.debug("Valid certificate subject CN for '%s': '%s'"
% (self.hostname, commonName))
return True
104 changes: 104 additions & 0 deletions lib/bridgedb/test/test_crypto.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,16 +14,38 @@
from __future__ import print_function
from __future__ import unicode_literals

import logging
import os

import OpenSSL

from twisted.internet import defer
from twisted.trial import unittest
from twisted.test.proto_helpers import StringTransport
from twisted.web.test import test_agent as txtagent

from bridgedb import crypto
from bridgedb import txrecaptcha


logging.disable(50)

SEKRIT_KEY = b'v\x16Xm\xfc\x1b}\x063\x85\xaa\xa5\xf9\xad\x18\xb2P\x93\xc6k\xf9'
SEKRIT_KEY += b'\x8bI\xd9\xb8xw\xf5\xec\x1b\x7f\xa8'


class DummyEndpoint(object):
"""An endpoint that uses a fake transport."""

def connect(self, factory):
"""Returns a connection to a
:api:`twisted.test.proto_helpers.StringTransport`.
"""
protocol = factory.buildProtocol(None)
protocol.makeConnection(StringTransport())
return defer.succeed(protocol)


class GetKeyTests(unittest.TestCase):
"""Tests for :func:`bridgedb.crypto.getKey`."""

Expand Down Expand Up @@ -56,3 +78,85 @@ def test_getKey_keyexists(self):
key (in hex): %s
SEKRIT_KEY (in hex): %s"""
% (key.encode('hex'), SEKRIT_KEY.encode('hex')))


class SSLVerifyingContextFactoryTests(unittest.TestCase,
txtagent.FakeReactorAndConnectMixin):
"""Tests for :class:`bridgedb.crypto.SSLVerifyingContextFactory`."""

_certificateText = (
"-----BEGIN CERTIFICATE-----\n"
"MIIEdjCCA16gAwIBAgIITcyHZlE/AhQwDQYJKoZIhvcNAQEFBQAwSTELMAkGA1UE\n"
"BhMCVVMxEzARBgNVBAoTCkdvb2dsZSBJbmMxJTAjBgNVBAMTHEdvb2dsZSBJbnRl\n"
"cm5ldCBBdXRob3JpdHkgRzIwHhcNMTQwMjEyMTUxMTE2WhcNMTQwNjEyMDAwMDAw\n"
"WjBoMQswCQYDVQQGEwJVUzETMBEGA1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwN\n"
"TW91bnRhaW4gVmlldzETMBEGA1UECgwKR29vZ2xlIEluYzEXMBUGA1UEAwwOd3d3\n"
"Lmdvb2dsZS5jb20wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCt3TOf\n"
"VOf4vfy4IROcEyiFzAJA+B3xkMccwA4anaD6VyGSFglRn5Oht3t+G0Mnu/LMuGba\n"
"EE6NEBEUEbH8KMlAcVRj58LoFIzulaRCdkVX7JK9R+kU05sggvIl1Q2quaWSjiMQ\n"
"SpyvKz1I2cmU5Gm4MfW/66M5ZJO323VrV19ydrgAtdbNnvVj85asrSyzwEBNxzNC\n"
"N6OQtOmTt4I7KLXqkROtTmTFvhAGBsvhG0hJZWhoP1aVsFO+KcE2OaIIxWQ4ckW7\n"
"BJEgYaXfgHo01LdR55aevGUqLfsdyT+GMZrG9k7eqAw4cq3ML2Y6RiyzskqoQL30\n"
"3OdYjKTIcU+i3BoFAgMBAAGjggFBMIIBPTAdBgNVHSUEFjAUBggrBgEFBQcDAQYI\n"
"KwYBBQUHAwIwGQYDVR0RBBIwEIIOd3d3Lmdvb2dsZS5jb20waAYIKwYBBQUHAQEE\n"
"XDBaMCsGCCsGAQUFBzAChh9odHRwOi8vcGtpLmdvb2dsZS5jb20vR0lBRzIuY3J0\n"
"MCsGCCsGAQUFBzABhh9odHRwOi8vY2xpZW50czEuZ29vZ2xlLmNvbS9vY3NwMB0G\n"
"A1UdDgQWBBQN7uQBzGDjvKRna111g9iPPtaXVTAMBgNVHRMBAf8EAjAAMB8GA1Ud\n"
"IwQYMBaAFErdBhYbvPZotXb1gba7Yhq6WoEvMBcGA1UdIAQQMA4wDAYKKwYBBAHW\n"
"eQIFATAwBgNVHR8EKTAnMCWgI6Ahhh9odHRwOi8vcGtpLmdvb2dsZS5jb20vR0lB\n"
"RzIuY3JsMA0GCSqGSIb3DQEBBQUAA4IBAQBrVp/xys2ABQvWPxpVrYaXiaoBXdxu\n"
"RVVXp5Lyu8IipKqFJli81hOX9eqPG7biYeph9HiKnW31xsXebaVlWWL3NXOh5X83\n"
"wpzozL0AkxskTMHQknrbIGLtmG67H71aKYyCthHEjawLmYjjvkcF6f9fKdYENM4C\n"
"skz/yjtlPBQFAuT6J9w0b3qtc42sHNlpgIOdIRQc2YCD0p6jAo+wKjoRuRu3ILKj\n"
"oCVrOPbDMPN4a2gSmK8Ur0aHuEpcNghg6HJsVSANokIIwQ/r4niqL5yotsangP/5\n"
"rR97EIYKFz7C6LMy/PIe8xFTIyKMtM59IcpUDIwCLlM9JtNdwN4VpyKy\n"
"-----END CERTIFICATE-----\n")

def setUp(self):
"""Create a fake reactor for these tests."""
self.reactor = self.Reactor()
self.url = 'https://www.example.com/someresource.html#andatag'

def test_getHostnameFromURL(self):
"""``getHostnameFromURL()`` should return a hostname from a URI."""
agent = txrecaptcha._getAgent(self.reactor, self.url)
contextFactory = agent._contextFactory
self.assertRegexpMatches(contextFactory.hostname,
'.*www\.example\.com')

def test_verifyHostname_mismatching(self):
"""Check that ``verifyHostname()`` returns ``False`` when the
``SSLVerifyingContextFactory.hostname`` does not match the one found
in the level 0 certificate subject CN.
"""
agent = txrecaptcha._getAgent(self.reactor, self.url)
contextFactory = agent._contextFactory
x509 = OpenSSL.crypto.load_certificate(OpenSSL.crypto.FILETYPE_PEM,
self._certificateText)
conn = DummyEndpoint()
result = contextFactory.verifyHostname(conn, x509, 0, 0, True)
self.assertIs(result, False)

def test_verifyHostname_matching(self):
"""Check that ``verifyHostname()`` returns ``True`` when the
``SSLVerifyingContextFactory.hostname`` matches the one found in the
level 0 certificate subject CN.
"""
hostname = 'www.google.com'
url = 'https://' + hostname + '/recaptcha'
contextFactory = crypto.SSLVerifyingContextFactory(url)
self.assertEqual(contextFactory.hostname, hostname)

x509 = OpenSSL.crypto.load_certificate(OpenSSL.crypto.FILETYPE_PEM,
self._certificateText)
conn = DummyEndpoint()
result = contextFactory.verifyHostname(conn, x509, 0, 0, True)
self.assertTrue(result)

def test_getContext(self):
"""The context factory's ``getContext()`` method should produce an
``OpenSSL.SSL.Context`` object.
"""
contextFactory = crypto.SSLVerifyingContextFactory(self.url)
self.assertIsInstance(contextFactory.getContext(),
OpenSSL.SSL.Context)

0 comments on commit 8d927c1

Please sign in to comment.