Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Replace pyopenssl with cryptography module and add parsing of SAN extension #81

Merged
merged 2 commits into from
Apr 19, 2017
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
Changelog
=========
* next release
Copy link
Collaborator

@danielquinn danielquinn Apr 3, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When you're ready to do a release, I think that semver would dictate that this should be at least 1.2 if not 2.0.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i think we can get away with 1.2, if people do install -U they will get new lib and their stuff will not have to change.
They will just be more accurate, since classes attributes stayed the same from what i see.

* Replaced pyOpenSSL with cryptography
* Added parsing of subjectAltName X509 extension
* 1.1.11
* Added first version of WiFi results
* 1.1.10
Expand Down
8 changes: 4 additions & 4 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,7 @@ Troubleshooting

Some setups (like MacOS) have trouble with building the dependencies required
for reading SSL certificates. If you don't care about SSL stuff and only want to
use sagan to say, parse traceroute or DNS results, then you can tell the
installer to skip building ``pyOpenSSL`` by doing the following:
use sagan to say, parse traceroute or DNS results, then you can do the following:

.. code:: bash

Expand Down Expand Up @@ -175,9 +174,10 @@ What it requires
As you might have guessed, with all of this magic going on under the hood, there
are a few dependencies:

- `pyOpenSSL`_ (Optional: see "Troubleshooting" above)
- `cryptography`_ (Optional: see "Troubleshooting" above)
- `python-dateutil`_
- `pytz`_
- `IPy`_

Additionally, we recommend that you also install `ujson`_ as it will speed up
the JSON-decoding step considerably, and `sphinx`_ if you intend to build the
Expand Down Expand Up @@ -229,9 +229,9 @@ But why "`Sagan`_"? The RIPE Atlas team decided to name all of its modules after
explorers, and what better name for a parser than that of the man who spent
decades reaching out to the public about the wonders of the cosmos?

.. _pyOpenSSL: https://pypi.python.org/pypi/pyOpenSSL
.. _python-dateutil: https://pypi.python.org/pypi/python-dateutil
.. _pytz: https://pypi.python.org/pypi/pytz
.. _IPy: https://pypi.python.org/pypi/IPy/
.. _ujson: https://pypi.python.org/pypi/ujson
.. _sphinx: https://pypi.python.org/pypi/Sphinx
.. _Read the Docs: http://ripe-atlas-sagan.readthedocs.org/en/latest/
Expand Down
7 changes: 3 additions & 4 deletions docs/installation.rst
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,15 @@ Requirements
As you might have guessed, with all of the magic going on under the hood, there
are a few dependencies:

* `pyOpenSSL`_
* `cryptography`_
* `python-dateutil`_
* `pytz`_

Additionally, we recommend that you also install `ujson`_ as it will speed up
the JSON-decoding step considerably, and `sphinx`_ if you intend to build the
documentation files for offline use.

.. _pyOpenSSL: https://pypi.python.org/pypi/pyOpenSSL/
.. _cryptography: https://pypi.python.org/pypi/cryptography
.. _python-dateutil: https://pypi.python.org/pypi/python-dateutil/
.. _pytz: https://pypi.python.org/pypi/pytz/
.. _ujson: https://pypi.python.org/pypi/ujson/
Expand Down Expand Up @@ -75,8 +75,7 @@ Troubleshooting

Some setups (like MacOS) have trouble with building the dependencies required
for reading SSL certificates. If you don't care about SSL stuff and only want
to use sagan to say, parse traceroute or DNS results, then you can tell the
installer to skip building ``pyOpenSSL`` by doing the following::
to use sagan to say, parse traceroute or DNS results, then you can do the following:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This may require additional explanation or none at all and probably needs a little experimentation on different platforms to be sure. PyOpenSSL was a pain in the ass to install on a Mac, but I'm not sure if that's also the case with cryptography. I know that in Linux environments, cryptography requires certain system-level libraries to be installed, and I'm sure that Mac & Windows each have their own quirks.

The good news however is that this is all beautifully documented by the cryptography team here so you might want to adjust this paragraph to just give the user a heads-up and point them to that URL if they have trouble.


$ SAGAN_WITHOUT_SSL=1 pip install ripe.atlas.sagan

Expand Down
1 change: 1 addition & 0 deletions docs/types.rst
Original file line number Diff line number Diff line change
Expand Up @@ -696,6 +696,7 @@ checksum_md5 str The md5 checksum
checksum_sha1 str The sha1 checksum
checksum_sha256 str The sha256 checksum
has_expired bool Set to ``True`` if the certificate is no longer valid
extensions dict Parsed extensions. For now it can only be subjectAltName, which is a list of names contained in the SAN extension, if that exists.
===================== ======== ===================================================================================


Expand Down
150 changes: 54 additions & 96 deletions ripe/atlas/sagan/ssl.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,16 +15,17 @@

import logging
import pytz
import re
import codecs

from datetime import datetime
from dateutil.relativedelta import relativedelta

try:
import OpenSSL
from cryptography import x509
from cryptography.hazmat.backends import openssl
from cryptography.hazmat.primitives import hashes
except ImportError:
logging.warning(
"pyOpenSSL is not installed, without it you cannot parse SSL "
"cryptography module is not installed, without it you cannot parse SSL "
"certificate measurement results"
)

Expand All @@ -33,11 +34,6 @@

class Certificate(ParsingDict):

TIME_FORMAT = "%Y%m%d%H%M%SZ"
TIME_REGEX = re.compile(
"(\d\d\d\d)(\d\d)(\d\d)(\d\d)(\d\d)(\d\d)(\+|\-)(\d\d)(\d\d)"
)

def __init__(self, data, **kwargs):

ParsingDict.__init__(self, **kwargs)
Expand All @@ -49,6 +45,7 @@ def __init__(self, data, **kwargs):
self.issuer_cn = None
self.issuer_o = None
self.issuer_c = None

self.valid_from = None
self.valid_until = None

Expand All @@ -58,32 +55,59 @@ def __init__(self, data, **kwargs):

self.has_expired = None

# Clean up the certificate data and use OpenSSL to parse it
x509 = OpenSSL.crypto.load_certificate(
OpenSSL.crypto.FILETYPE_PEM,
data.replace("\\/", "/").replace("\n\n", "\n")
)
subject = dict(x509.get_subject().get_components())
issuer = dict(x509.get_issuer().get_components())
self.extensions = {}

cert = x509.load_pem_x509_certificate(data.encode('ascii'), openssl.backend)

if cert:
self.checksum_md5 = self._colonify(cert.fingerprint(hashes.MD5()))
self.checksum_sha1 = self._colonify(cert.fingerprint(hashes.SHA1()))
self.checksum_sha256 = self._colonify(cert.fingerprint(hashes.SHA256()))

self.valid_from = pytz.utc.localize(cert.not_valid_before)
self.valid_until = pytz.utc.localize(cert.not_valid_after)

if x509 and subject and issuer:
self.has_expired = self._has_expired()

self.subject_cn = self._string_from_dict_or_none(subject, b"CN")
self.subject_o = self._string_from_dict_or_none(subject, b"O")
self.subject_c = self._string_from_dict_or_none(subject, b"C")
self.issuer_cn = self._string_from_dict_or_none(issuer, b"CN")
self.issuer_o = self._string_from_dict_or_none(issuer, b"O")
self.issuer_c = self._string_from_dict_or_none(issuer, b"C")
self._add_extensions(cert)

self.checksum_md5 = x509.digest("md5").decode()
self.checksum_sha1 = x509.digest("sha1").decode()
self.checksum_sha256 = x509.digest("sha256").decode()
if cert and cert.subject:
self.subject_cn, self.subject_o, self.subject_c = \
self._parse_x509_name(cert.subject)

self.has_expired = bool(x509.has_expired())
if cert and cert.issuer:
self.issuer_cn, self.issuer_o, self.issuer_c = \
self._parse_x509_name(cert.issuer)

self.valid_from = None
self.valid_until = None
self._process_validation_times(x509)
def _add_extensions(self, cert):
for ext in cert.extensions:
if ext.oid._name == 'subjectAltName':
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: the rest of the module is using double quotes for strings. It'd be nice to stay consistent.

self.extensions['subjectAltName'] = []
for san in ext.value:
self.extensions['subjectAltName'].append(san.value)

@staticmethod
def _colonify(bytes):
hex = codecs.getencoder('hex_codec')(bytes)[0].decode('ascii').upper()
return ':'.join(a+b for a,b in zip(hex[::2], hex[1::2]))

@staticmethod
def _parse_x509_name(name):
cn = None
o = None
c = None
for attr in name:
if attr.oid.dotted_string == '2.5.4.6': # country
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Arbitrary strings like "2.5.4.6" don't make a lot of sense out of context like this, so it might be nicer to place them in a class constant like OLD_STYLE_COUNTRY="2.5.4.6" (or a more appropriate name). I don't really get what's going on here, but maybe that's just my ignorance of arcane SSL rules. If it's particularly weird though, a docstring on the function might also be nice.

c = attr.value
elif attr.oid.dotted_string == '2.5.4.10': # organisation
o = attr.value
elif attr.oid.dotted_string == '2.5.4.3': # common name
cn = attr.value
return cn, o, c

def _has_expired(self):
now = pytz.utc.localize(datetime.utcnow())
return self.valid_from <= now <= self.valid_until

@property
def cn(self):
Expand Down Expand Up @@ -113,72 +137,6 @@ def country(self):
def checksum(self):
return self.checksum_sha256

def _process_validation_times(self, x509):
"""
PyOpenSSL uses a kooky date format that *usually* parses out quite
easily but on the off chance that it's not in UTC, a lot of work needs
to be done.
"""

valid_from = x509.get_notBefore()
valid_until = x509.get_notAfter()

try:
self.valid_from = pytz.UTC.localize(datetime.strptime(
valid_from.decode(),
self.TIME_FORMAT
))
except ValueError:
self.valid_from = self._process_nonstandard_time(valid_from)

try:
self.valid_until = pytz.UTC.localize(datetime.strptime(
valid_until.decode(),
self.TIME_FORMAT
))
except ValueError:
self.valid_until = self._process_nonstandard_time(valid_until)

def _process_nonstandard_time(self, string):
"""
In addition to `YYYYMMDDhhmmssZ`, PyOpenSSL can also use timestamps
in `YYYYMMDDhhmmss+hhmm` or `YYYYMMDDhhmmss-hhmm`.
"""

match = re.match(self.TIME_REGEX, string)

if not match:
raise ResultParseError(
"Unrecognised time format: {s}".format(s=string)
)

r = datetime(
int(match.group(1)),
int(match.group(2)),
int(match.group(3)),
int(match.group(4)),
int(match.group(5)),
int(match.group(6)),
0,
pytz.UTC
)
delta = relativedelta(
hours=int(match.group(8)),
minutes=int(match.group(9))
)
if match.group(7) == "-":
return r - delta
return r + delta

@staticmethod
def _string_from_dict_or_none(dictionary, key):
"""
Created to make nice with the Python3 problem.
"""
if key not in dictionary:
return None
return dictionary[key].decode("UTF-8")


class Alert(ParsingDict):

Expand Down
4 changes: 2 additions & 2 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,15 @@

name = "ripe.atlas.sagan"
install_requires = [
"IPy",
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This isn't part of this issue is it? If not, it should probably be in a separate commit, if only to keep future developers from conflating IPy with the addition of cryptography. The same goes for the documentation references to IPy above.

"python-dateutil",
"pytz",
]

tests_require = ["nose"]

# pyOpenSSL support is flaky on some systems (I'm looking at you Apple)
if "SAGAN_WITHOUT_SSL" not in os.environ:
install_requires.append("pyOpenSSL")
install_requires.append("cryptography")

# Allow setup.py to be run from any path
os.chdir(os.path.normpath(os.path.join(os.path.abspath(__file__), os.pardir)))
Expand Down
6 changes: 6 additions & 0 deletions tests/ssl.py
Original file line number Diff line number Diff line change
Expand Up @@ -386,3 +386,9 @@ def test_alert():
result = Result.get('{"af":4,"cert":["-----BEGIN CERTIFICATE-----\\nMIIFBTCCAu2gAwIBAgIDDLHHMA0GCSqGSIb3DQEBBQUAMHkxEDAOBgNVBAoTB1Jv\\nb3QgQ0ExHjAcBgNVBAsTFWh0dHA6Ly93d3cuY2FjZXJ0Lm9yZzEiMCAGA1UEAxMZ\\nQ0EgQ2VydCBTaWduaW5nIEF1dGhvcml0eTEhMB8GCSqGSIb3DQEJARYSc3VwcG9y\\ndEBjYWNlcnQub3JnMB4XDTEzMDEwNjE0MDA1NVoXDTEzMDcwNTE0MDA1NVowGDEW\\nMBQGA1UEAxQNKi5wcmV0aWNhbC5lZTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCC\\nAQoCggEBAMS+vX7gA8TvzFwxryFFRj1OyQjnW88GvfMuGhKJopalG1EB103oRsxi\\nMcXqwFZUicpqLKHW4lCHcRuhpKoZp8EOILnRAJRKFOjgIrcHQ02Xn4Lf/ewl601h\\n5qxqt1keU1P8j+u9m7zZN+vOoNlEKZ5SnZhysAAYqr/XIt1WY2cji/4GxjF+q1OH\\nIl5zddkIfnE52UbREKKlIakfFdj/c6GXqqsP2QTmm4x2HitCD964tZ06fA9BitQj\\nnnBXNhtm2MCuBIPBSq0/C7LREwmfnqxCFqE7iqEPNIQ2IT2D4Gh4c+nIZHqYKvCV\\nP3zh3aUaBj1o5Lo83IDdXCKAIiQRFMkCAwEAAaOB9jCB8zAMBgNVHRMBAf8EAjAA\\nMA4GA1UdDwEB/wQEAwIDqDA0BgNVHSUELTArBggrBgEFBQcDAgYIKwYBBQUHAwEG\\nCWCGSAGG+EIEAQYKKwYBBAGCNwoDAzAzBggrBgEFBQcBAQQnMCUwIwYIKwYBBQUH\\nMAGGF2h0dHA6Ly9vY3NwLmNhY2VydC5vcmcvMDEGA1UdHwQqMCgwJqAkoCKGIGh0\\ndHA6Ly9jcmwuY2FjZXJ0Lm9yZy9yZXZva2UuY3JsMDUGA1UdEQQuMCyCDSoucHJl\\ndGljYWwuZWWgGwYIKwYBBQUHCAWgDwwNKi5wcmV0aWNhbC5lZTANBgkqhkiG9w0B\\nAQUFAAOCAgEAycddS/c47eo0WVrFxpvCIJdfvn7CYdTPpXNSg0kjapkSjYuAkcmq\\nsrScUUGMBe6tfkmkdPTuNKwRVYNJ1Wi9EYaMvJ3CVw6x9O5mgktmu0ogbIXsivwI\\nTSzGDMWcb9Of85e/ALWpK0cWIugtWO0d6v3qMWfxlYfAaYu49pttOJQOjbXAAhfR\\njE5VOcDaIlWChG48jLAyCLsMwHlyLw8D5Myb9MfTs1XxgLESO9ZTSqGEqJw+BwTJ\\nstHk/oCHo9FL/Xv5MmFcNaTpqbB60duYJ+DLLX1QiRRfLJ18G7wEiEAm6H9egupQ\\nL9MhQQLJ4o60xTrCnpqGTXTSR16jiTm70bDB0+SU3xTpNwCzdigH6ceKbPIr0cO6\\no0ump598e2JNCPsXIc+XcbLDDFgYrlnl3YnK3J+K3LC7SWPMsYdDfe+Im880tNuW\\nOlnOCDpP8408KqCp4xm0DMznmThUM34/Ia+o8Q3NWNBfuaOsJ9aA+FmgobJhih9e\\nUr9x3ByRQXcW5Cs/AMtCikKWVPsx+IA5eeyt+1i+dKBWksO40B3ADsq1O5DRYYRa\\n+dwqdX/jduqZjbyHuFH04q28j4zVDviUBQEa9UQoDM3c82dILDjbYtZ+T28sPMTa\\nbMZdcMur9E+ovrS58lIKGCvDEPSUDXHzr0tpb4A13TTnxW6pclqUyJk=\\n-----END CERTIFICATE-----"],"dst_addr":"80.79.115.54","dst_name":"pretical.ee","dst_port":"https","from":"77.95.64.18","fw":4480,"method":"SSL","msm_id":1006864,"prb_id":517,"src_addr":"77.95.64.18","timestamp":1362454627,"type":"sslcert","ver":"3.0"}')
assert(result.alert is None)
assert(result.is_error is False)

def test_san_extension():
result = Result.get('{"af":4,"cert":["-----BEGIN CERTIFICATE-----\nMIIH4jCCBsqgAwIBAgIIFaqhpQEYRXIwDQYJKoZIhvcNAQELBQAwSTELMAkGA1UE\nBhMCVVMxEzARBgNVBAoTCkdvb2dsZSBJbmMxJTAjBgNVBAMTHEdvb2dsZSBJbnRl\ncm5ldCBBdXRob3JpdHkgRzIwHhcNMTcwMzE2MDkzNzQyWhcNMTcwNjA4MDg1NDAw\nWjBmMQswCQYDVQQGEwJVUzETMBEGA1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwN\nTW91bnRhaW4gVmlldzETMBEGA1UECgwKR29vZ2xlIEluYzEVMBMGA1UEAwwMKi5n\nb29nbGUuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAjgPs3rpA\ntF2jQzXrVQ8x33EVHB3OIpj3GcwVf8U9qcPce0XuG97fHInb20U9Uw1b45ecNRtn\nWLUw14/7+F4cvFJXHHsYaoUdBoeSJAcOy8ktgxvIEMk82KJwJlzWA7X7B459Fy1U\nr8Dvu6dNFzhtu8eJs8bFOMJ/Wczjh8tylKXyWNMpotTbvAG3rGH+8fttmGXnztTB\n3dwxxf6SEL6m4XGH7POxwH9+AKzIwV9PrkU4JM5U2YsGPHf6ao/w27gPVpO5sh3g\nP9J/3jf8lXNwPZWSLCK5C2i7kz12ohaD7jlipVyw4nYLcEFPs27LwzjYa/YFU8VZ\nreIcbazBmDsqBwIDAQABo4IErzCCBKswHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsG\nAQUFBwMCMIIDewYDVR0RBIIDcjCCA26CDCouZ29vZ2xlLmNvbYINKi5hbmRyb2lk\nLmNvbYIWKi5hcHBlbmdpbmUuZ29vZ2xlLmNvbYISKi5jbG91ZC5nb29nbGUuY29t\ngg4qLmdjcC5ndnQyLmNvbYIWKi5nb29nbGUtYW5hbHl0aWNzLmNvbYILKi5nb29n\nbGUuY2GCCyouZ29vZ2xlLmNsgg4qLmdvb2dsZS5jby5pboIOKi5nb29nbGUuY28u\nanCCDiouZ29vZ2xlLmNvLnVrgg8qLmdvb2dsZS5jb20uYXKCDyouZ29vZ2xlLmNv\nbS5hdYIPKi5nb29nbGUuY29tLmJygg8qLmdvb2dsZS5jb20uY2+CDyouZ29vZ2xl\nLmNvbS5teIIPKi5nb29nbGUuY29tLnRygg8qLmdvb2dsZS5jb20udm6CCyouZ29v\nZ2xlLmRlggsqLmdvb2dsZS5lc4ILKi5nb29nbGUuZnKCCyouZ29vZ2xlLmh1ggsq\nLmdvb2dsZS5pdIILKi5nb29nbGUubmyCCyouZ29vZ2xlLnBsggsqLmdvb2dsZS5w\ndIISKi5nb29nbGVhZGFwaXMuY29tgg8qLmdvb2dsZWFwaXMuY26CFCouZ29vZ2xl\nY29tbWVyY2UuY29tghEqLmdvb2dsZXZpZGVvLmNvbYIMKi5nc3RhdGljLmNugg0q\nLmdzdGF0aWMuY29tggoqLmd2dDEuY29tggoqLmd2dDIuY29tghQqLm1ldHJpYy5n\nc3RhdGljLmNvbYIMKi51cmNoaW4uY29tghAqLnVybC5nb29nbGUuY29tghYqLnlv\ndXR1YmUtbm9jb29raWUuY29tgg0qLnlvdXR1YmUuY29tghYqLnlvdXR1YmVlZHVj\nYXRpb24uY29tggsqLnl0aW1nLmNvbYIaYW5kcm9pZC5jbGllbnRzLmdvb2dsZS5j\nb22CC2FuZHJvaWQuY29tghtkZXZlbG9wZXIuYW5kcm9pZC5nb29nbGUuY26CBGcu\nY2+CBmdvby5nbIIUZ29vZ2xlLWFuYWx5dGljcy5jb22CCmdvb2dsZS5jb22CEmdv\nb2dsZWNvbW1lcmNlLmNvbYIKdXJjaGluLmNvbYIKd3d3Lmdvby5nbIIIeW91dHUu\nYmWCC3lvdXR1YmUuY29tghR5b3V0dWJlZWR1Y2F0aW9uLmNvbTBoBggrBgEFBQcB\nAQRcMFowKwYIKwYBBQUHMAKGH2h0dHA6Ly9wa2kuZ29vZ2xlLmNvbS9HSUFHMi5j\ncnQwKwYIKwYBBQUHMAGGH2h0dHA6Ly9jbGllbnRzMS5nb29nbGUuY29tL29jc3Aw\nHQYDVR0OBBYEFHRy1woLF5IqQVubJZ5ZvXAjaJ0aMAwGA1UdEwEB/wQCMAAwHwYD\nVR0jBBgwFoAUSt0GFhu89mi1dvWBtrtiGrpagS8wIQYDVR0gBBowGDAMBgorBgEE\nAdZ5AgUBMAgGBmeBDAECAjAwBgNVHR8EKTAnMCWgI6Ahhh9odHRwOi8vcGtpLmdv\nb2dsZS5jb20vR0lBRzIuY3JsMA0GCSqGSIb3DQEBCwUAA4IBAQAsoPR1jJz2adkK\nTVXpGse/M3l+xKgmuZHpXzXkAiqE9wcsxXxCU3dEUmPBYYGRTqODNkOh9AMyGzIL\nfrYh/zY9rhqJ2B26OunmxKFF9BmwRi2rp+Ksvg/+27F57Hyaq2phSaR8E7hRZcYR\nYqCaNA5e1hialuB1g58mAvs38jxxV4bQhKzCKkBOxolhYbUEBEV4mQ14ODdSvAB0\n8L1dMjk3+LEDB/hWdtpOOhtMbSPa1u7xJeM/Ip7+GV47lS3V6rUALDKz4ASNk8ih\nX0ZmxPA1rabqNFutG8L/4HK2/ffO4bKEkHEdOQXC9B17n1x65fbLUbweDPDAzaow\nrum/OChG\n-----END CERTIFICATE-----"],"dst_addr":"193.0.6.158","dst_name":"atlas.ripe.net","dst_port":"443","from":"86.82.178.27","fw":4760,"lts":133,"method":"TLS","msm_id":14002,"msm_name":"SSLCert","prb_id":10951,"rt":51.558465,"src_addr":"192.168.180.22","timestamp":1490659208,"ttc":14.88238,"type":"sslcert","ver":"1.2"}')
ext = result.certificates[0].extensions
assert(ext and len(ext['subjectAltName'])==54)