-
Notifications
You must be signed in to change notification settings - Fork 25
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
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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/ | ||
|
@@ -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: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. The good news however is that this is all beautifully documented by the |
||
|
||
$ SAGAN_WITHOUT_SSL=1 pip install ripe.atlas.sagan | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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" | ||
) | ||
|
||
|
@@ -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) | ||
|
@@ -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 | ||
|
||
|
@@ -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': | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Arbitrary strings like |
||
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): | ||
|
@@ -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): | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -6,15 +6,15 @@ | |
|
||
name = "ripe.atlas.sagan" | ||
install_requires = [ | ||
"IPy", | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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))) | ||
|
There was a problem hiding this comment.
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 not2.0
.There was a problem hiding this comment.
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.