commit 6bbc11b2f0683d55390dfcc2c4409f51d866fe2e Author: agix columbeff@gmail.com Date: Thu Apr 29 15:15:56 2021 +0200
Fix for issue #27984
Changed the hostname check by using verify_certificate_hostname from the service identity package for complete hostname verification. Added wildcard certificates for testing and two additional test cases (test_verifyHostname_matching_wildcard & test_verifyHostname_matching_wildcard_multidns) --- bridgedb/crypto.py | 26 +++++++--- bridgedb/test/test_crypto.py | 118 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 138 insertions(+), 6 deletions(-)
diff --git a/bridgedb/crypto.py b/bridgedb/crypto.py index 9bdf729..b2db8b2 100644 --- a/bridgedb/crypto.py +++ b/bridgedb/crypto.py @@ -54,6 +54,10 @@ from Crypto.PublicKey import RSA from twisted.internet import ssl from twisted.python.procutils import which
+from service_identity.cryptography import verify_certificate_hostname +from cryptography.x509 import load_pem_x509_certificate +from service_identity import VerificationError, CertificateError, SubjectAltNameWarning + #: The hash digest to use for HMACs. DIGESTMOD = hashlib.sha1
@@ -340,16 +344,26 @@ class SSLVerifyingContextFactory(ssl.CertificateOptions): """ commonName = x509.get_subject().commonName logging.debug("Received cert at level %d: '%s'" % (depth, commonName)) + x509 = x509.to_cryptography()
# 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: + try: + verify_certificate_hostname(x509, self.hostname) + logging.debug("Valid certificate subject CN for '%s': '%s'" + % (self.hostname, commonName)) + return True + except VerificationError: 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 + except CertificateError: + logging.warn("Certificate contains invalid or unexpected data") + return False + except SubjectAltNameWarning: + logging.warn("Certificate contains no SAN, fallback to common name") + cn = commonName.replace('*', '.*') + hostnamesMatch = re.search(cn, self.hostname) + hostnamesMatch = True if hostnamesMatch else False + return hostnamesMatch \ No newline at end of file diff --git a/bridgedb/test/test_crypto.py b/bridgedb/test/test_crypto.py index f57d4f3..b1af96d 100644 --- a/bridgedb/test/test_crypto.py +++ b/bridgedb/test/test_crypto.py @@ -191,6 +191,90 @@ class SSLVerifyingContextFactoryTests(unittest.TestCase, "rR97EIYKFz7C6LMy/PIe8xFTIyKMtM59IcpUDIwCLlM9JtNdwN4VpyKy\n" "-----END CERTIFICATE-----\n")
+ _wildcardcertificate = ( + "-----BEGIN CERTIFICATE-----\n" + "MIIExjCCA66gAwIBAgIRAKy0RV8bAucyAgAAAABnMzYwDQYJKoZIhvcNAQELBQAw\n" + "QjELMAkGA1UEBhMCVVMxHjAcBgNVBAoTFUdvb2dsZSBUcnVzdCBTZXJ2aWNlczET\n" + "MBEGA1UEAxMKR1RTIENBIDFPMTAeFw0yMDA1MDUwODMyMjlaFw0yMDA3MjgwODMy\n" + "MjlaMGUxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlhMRYwFAYDVQQH\n" + "Ew1Nb3VudGFpbiBWaWV3MRMwEQYDVQQKEwpHb29nbGUgTExDMRQwEgYDVQQDDAsq\n" + "Lmdvb2dsZS5hdDBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABDuwtcBm/Zy4gj3P\n" + "aaBHMOHIypfqB7KBhbzJSux7ZQpuWhM4R4anNzOzjjrCyi4R+u2mkjeB/Bw6xzt8\n" + "ObtSEmWjggJdMIICWTAOBgNVHQ8BAf8EBAMCB4AwEwYDVR0lBAwwCgYIKwYBBQUH\n" + "AwEwDAYDVR0TAQH/BAIwADAdBgNVHQ4EFgQUOCAX+hzjxyujW0S+sRabV0fAspkw\n" + "HwYDVR0jBBgwFoAUmNH4bhDrz5vsYJ8YkBug630J/SswZAYIKwYBBQUHAQEEWDBW\n" + "MCcGCCsGAQUFBzABhhtodHRwOi8vb2NzcC5wa2kuZ29vZy9ndHMxbzEwKwYIKwYB\n" + "BQUHMAKGH2h0dHA6Ly9wa2kuZ29vZy9nc3IyL0dUUzFPMS5jcnQwIQYDVR0RBBow\n" + "GIILKi5nb29nbGUuYXSCCWdvb2dsZS5hdDAhBgNVHSAEGjAYMAgGBmeBDAECAjAM\n" + "BgorBgEEAdZ5AgUDMC8GA1UdHwQoMCYwJKAioCCGHmh0dHA6Ly9jcmwucGtpLmdv\n" + "b2cvR1RTMU8xLmNybDCCAQUGCisGAQQB1nkCBAIEgfYEgfMA8QB3ALIeBcyLos2K\n" + "IE6HZvkruYolIGdr2vpw57JJUy3vi5BeAAABceQt+OQAAAQDAEgwRgIhANY9slOG\n" + "xYI8TgPHZ4QdaJAOkOUf+0OMQb47glFk/MAJAiEAlmNt5gNHqEKOhZAU5FB/PF9v\n" + "P6sD8oLupRxHiEMTtLIAdgBep3P531bA57U2SH3QSeAyepGaDIShEhKEGHWWgXFF\n" + "WAAAAXHkLfiiAAAEAwBHMEUCIQDcvmEUZcDHDCvX2cikDl8xTfGcWvKETvQL02qt\n" + "L/hVlQIgfvzN2SEzT2+VwuzgGbmC8CljAjVwxKMRc+BzlB0yFh4wDQYJKoZIhvcN\n" + "AQELBQADggEBAHsyJM0yEViV/Vo09mwmP3LAyNhK4hGl3+ueZRr7PE3zkbXA3Sb3\n" + "JAbdpaQJIGUb5PeiTCB5e/Zem8N6qh4WZOi09LVoVUvkOcCUTSFayRzkyTZK1gT5\n" + "9d/RWL4BfptK++pdkHWT24SnXw/OfeZBd537LWwhGo4X60RRmPIfvxenUcXlUull\n" + "ZKYMh5B/GkcHAUf6m79lsR5K82ryplDwHr7IpfPSlJyLZn7Y4n3G9HdFeqUVEw4y\n" + "J7GslEdOd8C5IaO0xLiw+A7kkUJ16LOYKncmaMnIdhL7ibpQltd8x9VHwM7tEw3g\n" + "52qHhy4vviLHs2SClrxmkINyRvqd9c/GHcI=\n" + "-----END CERTIFICATE-----\n") + + _wildcardmultidnscertificate = ( + "-----BEGIN CERTIFICATE-----\n" + "MIIJTzCCCDegAwIBAgIQGoaLDa+bxzQIAAAAAD69lzANBgkqhkiG9w0BAQsFADBC\n" + "MQswCQYDVQQGEwJVUzEeMBwGA1UEChMVR29vZ2xlIFRydXN0IFNlcnZpY2VzMRMw\n" + "EQYDVQQDEwpHVFMgQ0EgMU8xMB4XDTIwMDUwNTA4MjIzNVoXDTIwMDcyODA4MjIz\n" + "NVowZjELMAkGA1UEBhMCVVMxEzARBgNVBAgTCkNhbGlmb3JuaWExFjAUBgNVBAcT\n" + "DU1vdW50YWluIFZpZXcxEzARBgNVBAoTCkdvb2dsZSBMTEMxFTATBgNVBAMMDCou\n" + "Z29vZ2xlLmNvbTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABMExBCpkvy8VuVVJ\n" + "gyrY19Q/NvVPzb+yNS371xwzVriYLDhyEyGXNf45D/qj7qIe9EqkpOylAUvF5+4x\n" + "NYr5INujggbmMIIG4jAOBgNVHQ8BAf8EBAMCB4AwEwYDVR0lBAwwCgYIKwYBBQUH\n" + "AwEwDAYDVR0TAQH/BAIwADAdBgNVHQ4EFgQUn9s6xwZqJNPn2S7GBvgewUShDNcw\n" + "HwYDVR0jBBgwFoAUmNH4bhDrz5vsYJ8YkBug630J/SswZAYIKwYBBQUHAQEEWDBW\n" + "MCcGCCsGAQUFBzABhhtodHRwOi8vb2NzcC5wa2kuZ29vZy9ndHMxbzEwKwYIKwYB\n" + "BQUHMAKGH2h0dHA6Ly9wa2kuZ29vZy9nc3IyL0dUUzFPMS5jcnQwggSoBgNVHREE\n" + "ggSfMIIEm4IMKi5nb29nbGUuY29tgg0qLmFuZHJvaWQuY29tghYqLmFwcGVuZ2lu\n" + "ZS5nb29nbGUuY29tggkqLmJkbi5kZXaCEiouY2xvdWQuZ29vZ2xlLmNvbYIYKi5j\n" + "cm93ZHNvdXJjZS5nb29nbGUuY29tggYqLmcuY2+CDiouZ2NwLmd2dDIuY29tghEq\n" + "LmdjcGNkbi5ndnQxLmNvbYIKKi5nZ3BodC5jboIOKi5na2VjbmFwcHMuY26CFiou\n" + "Z29vZ2xlLWFuYWx5dGljcy5jb22CCyouZ29vZ2xlLmNhggsqLmdvb2dsZS5jbIIO\n" + "Ki5nb29nbGUuY28uaW6CDiouZ29vZ2xlLmNvLmpwgg4qLmdvb2dsZS5jby51a4IP\n" + "Ki5nb29nbGUuY29tLmFygg8qLmdvb2dsZS5jb20uYXWCDyouZ29vZ2xlLmNvbS5i\n" + "coIPKi5nb29nbGUuY29tLmNvgg8qLmdvb2dsZS5jb20ubXiCDyouZ29vZ2xlLmNv\n" + "bS50coIPKi5nb29nbGUuY29tLnZuggsqLmdvb2dsZS5kZYILKi5nb29nbGUuZXOC\n" + "CyouZ29vZ2xlLmZyggsqLmdvb2dsZS5odYILKi5nb29nbGUuaXSCCyouZ29vZ2xl\n" + "Lm5sggsqLmdvb2dsZS5wbIILKi5nb29nbGUucHSCEiouZ29vZ2xlYWRhcGlzLmNv\n" + "bYIPKi5nb29nbGVhcGlzLmNughEqLmdvb2dsZWNuYXBwcy5jboIUKi5nb29nbGVj\n" + "b21tZXJjZS5jb22CESouZ29vZ2xldmlkZW8uY29tggwqLmdzdGF0aWMuY26CDSou\n" + "Z3N0YXRpYy5jb22CEiouZ3N0YXRpY2NuYXBwcy5jboIKKi5ndnQxLmNvbYIKKi5n\n" + "dnQyLmNvbYIUKi5tZXRyaWMuZ3N0YXRpYy5jb22CDCoudXJjaGluLmNvbYIQKi51\n" + "cmwuZ29vZ2xlLmNvbYITKi53ZWFyLmdrZWNuYXBwcy5jboIWKi55b3V0dWJlLW5v\n" + "Y29va2llLmNvbYINKi55b3V0dWJlLmNvbYIWKi55b3V0dWJlZWR1Y2F0aW9uLmNv\n" + "bYIRKi55b3V0dWJla2lkcy5jb22CByoueXQuYmWCCyoueXRpbWcuY29tghphbmRy\n" + "b2lkLmNsaWVudHMuZ29vZ2xlLmNvbYILYW5kcm9pZC5jb22CG2RldmVsb3Blci5h\n" + "bmRyb2lkLmdvb2dsZS5jboIcZGV2ZWxvcGVycy5hbmRyb2lkLmdvb2dsZS5jboIE\n" + "Zy5jb4IIZ2dwaHQuY26CDGdrZWNuYXBwcy5jboIGZ29vLmdsghRnb29nbGUtYW5h\n" + "bHl0aWNzLmNvbYIKZ29vZ2xlLmNvbYIPZ29vZ2xlY25hcHBzLmNughJnb29nbGVj\n" + "b21tZXJjZS5jb22CGHNvdXJjZS5hbmRyb2lkLmdvb2dsZS5jboIKdXJjaGluLmNv\n" + "bYIKd3d3Lmdvby5nbIIIeW91dHUuYmWCC3lvdXR1YmUuY29tghR5b3V0dWJlZWR1\n" + "Y2F0aW9uLmNvbYIPeW91dHViZWtpZHMuY29tggV5dC5iZTAhBgNVHSAEGjAYMAgG\n" + "BmeBDAECAjAMBgorBgEEAdZ5AgUDMC8GA1UdHwQoMCYwJKAioCCGHmh0dHA6Ly9j\n" + "cmwucGtpLmdvb2cvR1RTMU8xLmNybDCCAQUGCisGAQQB1nkCBAIEgfYEgfMA8QB3\n" + "ALIeBcyLos2KIE6HZvkruYolIGdr2vpw57JJUy3vi5BeAAABceQk7HEAAAQDAEgw\n" + "RgIhAMeaPA4eH4PQ6P80cE7pV9+cA+JzHsfEHXybUpJYTUUqAiEAtt6UgO7o9Bo5\n" + "T83b8KngOaFoAMEitVL5ckcpZHNtOaQAdgBep3P531bA57U2SH3QSeAyepGaDISh\n" + "EhKEGHWWgXFFWAAAAXHkJOyPAAAEAwBHMEUCIQDLXewTtt5w1Pykw4iJLVEBNWsU\n" + "8LazX+MIkUmuOpwJQwIgIXySwReiZv9FbIJ/73ytICnrPGNkD43+PVMge9cJ0gQw\n" + "DQYJKoZIhvcNAQELBQADggEBAM/x3sGLTHpoan0gd9XdVCG6HsQynQEzLL25kTvN\n" + "q14pRburIrsdFWhtEETqNJo7IG19PozR6IdJkQqEBYjw6D9rRniikWeski5MhcFD\n" + "fU3ycQ2I2ISlAd3HelYNbVDJ8zJWerEGk7dyi2O/UmRreXHhi+dry8IgTiSssYNA\n" + "fht9x4GVvFTT6gap4E1Qy5UFMBGMqXmhXNoH9bYX1oY6N+8xr5tkTSQfV0T1kXKJ\n" + "WUhnDkwMUnvwuMUEP7elcvp7OgOslSQ5zwjnR3zSDozO072YR+L9SGMICCZz1ha1\n" + "DMCTA95gzVKezFCaUidRU9UyHOFzltfYDt7HRlp7MwWoPLM=\n" + "-----END CERTIFICATE-----\n") + def setUp(self): """Create a fake reactor for these tests.""" self.reactor = self.createReactor() @@ -248,6 +332,40 @@ class SSLVerifyingContextFactoryTests(unittest.TestCase, result = contextFactory.verifyHostname(conn, x509, 0, 0, True) self.assertTrue(result)
+ def test_verifyHostname_matching_wildcard(self): + """Check that ``verifyHostname()`` returns ``True`` when the + ``SSLVerifyingContextFactory.hostname`` matches the one found in the + level 0 certificate subject SAN. The certificate includes a wildcard + for the common name & SAN. + """ + hostname = 'www.google.at' + url = 'https://' + hostname + '/recaptcha' + contextFactory = crypto.SSLVerifyingContextFactory(url) + self.assertEqual(contextFactory.hostname, hostname) + + x509 = OpenSSL.crypto.load_certificate(OpenSSL.crypto.FILETYPE_PEM, + self._wildcardcertificate) + conn = DummyEndpoint() + result = contextFactory.verifyHostname(conn, x509, 0, 0, True) + self.assertTrue(result) + + def test_verifyHostname_matching_wildcard_multidns(self): + """Check that ``verifyHostname()`` returns ``True`` when the + ``SSLVerifyingContextFactory.hostname`` matches the one found in the + level 0 certificate subject CN. This specific google.com certificate + inlcudes numerous SAN's. + """ + 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._wildcardmultidnscertificate) + 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.
tor-commits@lists.torproject.org