Improve certificate check to support SNI (#71)

* Fix the certificate check to support SNI
* Better tests for the certificate check
* Activate verbose output when running make test
* Add commenting on the spider test
This commit is contained in:
Marian Steinbach 2018-10-03 21:01:52 +02:00 committed by GitHub
parent ae6a2e83e9
commit 57f8dea4e0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 78 additions and 16 deletions

View File

@ -44,5 +44,5 @@ test: dockerimage
docker run --rm -ti \ docker run --rm -ti \
--entrypoint "python3" \ --entrypoint "python3" \
$(IMAGE) \ $(IMAGE) \
-m unittest discover -p '*_test.py' -m unittest discover -p '*_test.py' -v

View File

@ -4,6 +4,7 @@ Gathers information on the TLS/SSL certificate used by a server
from urllib.parse import urlparse from urllib.parse import urlparse
import logging import logging
import socket
import ssl import ssl
from datetime import datetime from datetime import datetime
from datetime import timezone from datetime import timezone
@ -36,21 +37,40 @@ class Checker(AbstractChecker):
} }
parsed = urlparse(url) parsed = urlparse(url)
port = 443
if parsed.port is not None:
port = parsed.port
try: try:
cert = ssl.get_server_certificate((parsed.hostname, 443)) #cert = ssl.get_server_certificate((parsed.hostname, port))
x509 = crypto.load_certificate(crypto.FILETYPE_PEM, cert)
result['serial_number'] = str(x509.get_serial_number())
nb = x509.get_notBefore().decode('utf-8') context = ssl.create_default_context()
na = x509.get_notAfter().decode('utf-8')
# parse '2018 06 27 00 00 00Z'
result['not_before'] = datetime(int(nb[0:4]), int(nb[4:6]), int(nb[6:8]), int(nb[8:10]), int(nb[10:12]), int(nb[12:14]), tzinfo=timezone.utc).isoformat()
result['not_after'] = datetime(int(na[0:4]), int(na[4:6]), int(na[6:8]), int(na[8:10]), int(na[10:12]), int(na[12:14]), tzinfo=timezone.utc).isoformat()
# decode and convert from bytes to unicode # get certificate with SNI
result['subject'] = dict([tuple(map(lambda x: x.decode('utf-8'), tup)) for tup in x509.get_subject().get_components()]) with socket.create_connection((parsed.hostname, port)) as sock:
result['issuer'] = dict([tuple(map(lambda x: x.decode('utf-8'), tup)) for tup in x509.get_issuer().get_components()]) with context.wrap_socket(sock, server_hostname=parsed.hostname) as sslsock:
der_cert = sslsock.getpeercert(True)
cert = ssl.DER_cert_to_PEM_cert(der_cert)
try:
x509 = crypto.load_certificate(crypto.FILETYPE_PEM, cert)
result['serial_number'] = str(x509.get_serial_number())
nb = x509.get_notBefore().decode('utf-8')
na = x509.get_notAfter().decode('utf-8')
# parse '2018 06 27 00 00 00Z'
result['not_before'] = datetime(int(nb[0:4]), int(nb[4:6]), int(nb[6:8]), int(nb[8:10]), int(nb[10:12]), int(nb[12:14]), tzinfo=timezone.utc).isoformat()
result['not_after'] = datetime(int(na[0:4]), int(na[4:6]), int(na[6:8]), int(na[8:10]), int(na[10:12]), int(na[12:14]), tzinfo=timezone.utc).isoformat()
# decode and convert from bytes to unicode
result['subject'] = dict([tuple(map(lambda x: x.decode('utf-8'), tup)) for tup in x509.get_subject().get_components()])
result['issuer'] = dict([tuple(map(lambda x: x.decode('utf-8'), tup)) for tup in x509.get_issuer().get_components()])
except Exception as e:
result['exception'] = {
'type': str(type(e)),
'message': str(e),
}
except Exception as e: except Exception as e:
result['exception'] = { result['exception'] = {

View File

@ -1,10 +1,13 @@
from checks import certificate from checks import certificate
from checks.config import Config from checks.config import Config
import unittest import unittest
from pprint import pprint
class TestCertificateChecker(unittest.TestCase): class TestCertificateChecker(unittest.TestCase):
def test_google(self): def test_google(self):
"""Load cert from a site that should work"""
url = 'https://www.google.com/' url = 'https://www.google.com/'
config = Config(urls=[url]) config = Config(urls=[url])
checker = certificate.Checker(config=config, previous_results={}) checker = certificate.Checker(config=config, previous_results={})
@ -14,6 +17,7 @@ class TestCertificateChecker(unittest.TestCase):
self.assertEqual(result[url]['issuer']['O'], 'Google Trust Services') self.assertEqual(result[url]['issuer']['O'], 'Google Trust Services')
def test_kaarst(self): def test_kaarst(self):
"""Real-workd example"""
url = 'https://www.gruenekaarst.de/' url = 'https://www.gruenekaarst.de/'
config = Config(urls=[url]) config = Config(urls=[url])
checker = certificate.Checker(config=config, previous_results={}) checker = certificate.Checker(config=config, previous_results={})
@ -22,6 +26,37 @@ class TestCertificateChecker(unittest.TestCase):
self.assertIsNone(result[url]['exception']) self.assertIsNone(result[url]['exception'])
self.assertEqual(result[url]['issuer']['O'], 'COMODO CA Limited') self.assertEqual(result[url]['issuer']['O'], 'COMODO CA Limited')
def test_tls_v_1_0(self):
"""Load a certificate for a TLS v1.0 server"""
url = 'https://tls-v1-0.badssl.com:1010/'
config = Config(urls=[url])
checker = certificate.Checker(config=config, previous_results={})
result = checker.run()
self.assertIn(url, result)
self.assertIsNone(result[url]['exception'])
self.assertEqual(result[url]['subject']['CN'], '*.badssl.com')
def test_tls_v_1_1(self):
"""Load a certificate for a TLS v1.1 server"""
url = 'https://tls-v1-1.badssl.com:1011/'
config = Config(urls=[url])
checker = certificate.Checker(config=config, previous_results={})
result = checker.run()
self.assertIn(url, result)
self.assertIsNone(result[url]['exception'])
self.assertEqual(result[url]['subject']['CN'], '*.badssl.com')
def test_tls_v_1_2(self):
"""Load a certificate for a TLS v1.2 server"""
url = 'https://tls-v1-2.badssl.com:1012/'
config = Config(urls=[url])
checker = certificate.Checker(config=config, previous_results={})
result = checker.run()
self.assertIn(url, result)
self.assertIsNone(result[url]['exception'])
self.assertEqual(result[url]['subject']['CN'], '*.badssl.com')
if __name__ == '__main__': if __name__ == '__main__':
unittest.main() unittest.main()

View File

@ -1,12 +1,19 @@
import unittest import unittest
from pprint import pprint
from spider.spider import check_and_rate_site from spider.spider import check_and_rate_site
from pprint import pprint class TestSpider(unittest.TestCase):
class TestSpiderr(unittest.TestCase): """
Simply calls the spider.check_and_rate_site function
with httpbin.org URLs. We don't assert a lot here,
but at least make sure that most of our code is executed
in tests.
"""
def test_url1(self): def test_html(self):
"""Loads a simple HTML web page"""
entry = { entry = {
"url": "https://httpbin.org/html", "url": "https://httpbin.org/html",