Commit 2fabc824 authored by Leo Pound Singer's avatar Leo Pound Singer

Add workaround for SleekXMPP X.509 date bug

parent 8eee56a9
Pipeline #102710 passed with stages
in 1 minute and 22 seconds
......@@ -2,7 +2,10 @@
## 0.0.6 (unreleased)
- No changes yet.
- Add a monkeypatch to work around an issue in SleekXMPP's parsing of dates
in X.509 certificates that has been fixed in git but is not in a SleekXMPP
release. This workaround allows us to unpin and depend on the latest
versions of pyasn1 and pyasn1-modules.
## 0.0.5 (2019-02-01)
......
......@@ -4,6 +4,7 @@ universal=1
[flake8]
exclude =
sleek_lvalert/_version.py
sleek_lvalert/extern
versioneer.py
ignore = D100,D103,D104,D107
......
......@@ -8,12 +8,15 @@ from safe_netrc import netrc as _netrc
from safe_netrc import NetrcParseError
import sleekxmpp
from . import _monkeypatch
from ._version import get_versions
__all__ = ('LVAlertClient',)
__version__ = get_versions()['version']
del get_versions
_monkeypatch.install()
log = logging.getLogger(__name__)
DEFAULT_SERVER = 'lvalert.cgca.uwm.edu'
......
"""Install monkeypatches for SleekXMPP.
Install monkeypatches to work around the following issue in SleekXMPP, which
has been fixed in git but is not in a release:
https://github.com/fritzy/SleekXMPP/pull/482
Remove this file if there is a release of SleekXMPP that includes this pull
request.
"""
from sleekxmpp.xmlstream import cert as _cert
from .extern.sleekxmpp.xmlstream import cert as _new_cert
def install():
_cert.extract_dates = _new_cert.extract_dates
_cert.extract_names = _new_cert.extract_names
"""Files copied from external projects."""
"""
The contents of this directory are copied verbatim from SleekXMPP, from this
pull request: https://github.com/fritzy/SleekXMPP/pull/482
Copyright (c) 2010 Nathanael C. Fritz
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
"""
import logging
from datetime import datetime, timedelta
# Make a call to strptime before starting threads to
# prevent thread safety issues.
datetime.strptime('1970-01-01 12:00:00', "%Y-%m-%d %H:%M:%S")
try:
from packaging import version
def check_version(ver1, ver2):
return version.parse(ver1) <= version.parse(ver2)
except ImportError:
from distutils.version import LooseVersion, StrictVersion
def check_version(ver1, ver2):
try:
return StrictVersion(ver1) <= StrictVersion(ver2)
except ValueError:
return LooseVersion(ver1) <= LooseVersion(ver1)
try:
from pyasn1 import __version__ as pyasn1_version
from pyasn1.codec.der import decoder, encoder
from pyasn1.type.univ import Any, ObjectIdentifier, OctetString
from pyasn1.type.char import BMPString, IA5String, UTF8String
from pyasn1.type.useful import GeneralizedTime
from pyasn1_modules.rfc2459 import (Certificate, DirectoryString,
SubjectAltName, GeneralNames,
GeneralName)
from pyasn1_modules.rfc2459 import id_ce_subjectAltName as SUBJECT_ALT_NAME
from pyasn1_modules.rfc2459 import id_at_commonName as COMMON_NAME
XMPP_ADDR = ObjectIdentifier('1.3.6.1.5.5.7.8.5')
SRV_NAME = ObjectIdentifier('1.3.6.1.5.5.7.8.7')
HAVE_PYASN1 = True
HAVE_PYASN1_4 = check_version('0.4.1', pyasn1_version)
except ImportError:
HAVE_PYASN1 = False
HAVE_PYASN1_4 = False
log = logging.getLogger(__name__)
class CertificateError(Exception):
pass
def decode_str(data):
encoding = 'utf-16-be' if isinstance(data, BMPString) else 'utf-8'
return bytes(data).decode(encoding)
def extract_names(raw_cert):
results = {'CN': set(),
'DNS': set(),
'SRV': set(),
'URI': set(),
'XMPPAddr': set()}
cert = decoder.decode(raw_cert, asn1Spec=Certificate())[0]
tbs = cert.getComponentByName('tbsCertificate')
subject = tbs.getComponentByName('subject')
extensions = tbs.getComponentByName('extensions') or []
# Extract the CommonName(s) from the cert.
for rdnss in subject:
for rdns in rdnss:
for name in rdns:
oid = name.getComponentByName('type')
value = name.getComponentByName('value')
if oid != COMMON_NAME:
continue
value = decoder.decode(value, asn1Spec=DirectoryString())[0]
value = decode_str(value.getComponent())
results['CN'].add(value)
# Extract the Subject Alternate Names (DNS, SRV, URI, XMPPAddr)
for extension in extensions:
oid = extension.getComponentByName('extnID')
if oid != SUBJECT_ALT_NAME:
continue
if HAVE_PYASN1_4:
value = extension.getComponentByName('extnValue')
else:
value = decoder.decode(
extension.getComponentByName('extnValue'),
asn1Spec=OctetString()
)[0]
sa_names = decoder.decode(value, asn1Spec=SubjectAltName())[0]
for name in sa_names:
name_type = name.getName()
if name_type == 'dNSName':
results['DNS'].add(decode_str(name.getComponent()))
if name_type == 'uniformResourceIdentifier':
value = decode_str(name.getComponent())
if value.startswith('xmpp:'):
results['URI'].add(value[5:])
elif name_type == 'otherName':
name = name.getComponent()
oid = name.getComponentByName('type-id')
value = name.getComponentByName('value')
if oid == XMPP_ADDR:
value = decoder.decode(value, asn1Spec=UTF8String())[0]
results['XMPPAddr'].add(decode_str(value))
elif oid == SRV_NAME:
value = decoder.decode(value, asn1Spec=IA5String())[0]
results['SRV'].add(decode_str(value))
return results
def extract_dates(raw_cert):
if not HAVE_PYASN1:
log.warning("Could not find pyasn1 and pyasn1_modules. " + \
"SSL certificate expiration COULD NOT BE VERIFIED.")
return None, None
cert = decoder.decode(raw_cert, asn1Spec=Certificate())[0]
tbs = cert.getComponentByName('tbsCertificate')
validity = tbs.getComponentByName('validity')
not_before = validity.getComponentByName('notBefore')
not_before = str(not_before.getComponent())
not_after = validity.getComponentByName('notAfter')
not_after = str(not_after.getComponent())
if HAVE_PYASN1_4:
not_before = datetime.strptime(not_before, '%y%m%d%H%M%SZ')
not_after = datetime.strptime(not_after, '%y%m%d%H%M%SZ')
else:
not_before = datetime.strptime(not_before, '%Y%m%d%H%M%SZ')
not_after = datetime.strptime(not_after, '%Y%m%d%H%M%SZ')
return not_before, not_after
def get_ttl(raw_cert):
not_before, not_after = extract_dates(raw_cert)
if not_after is None:
return None
return not_after - datetime.utcnow()
def verify(expected, raw_cert):
if not HAVE_PYASN1:
log.warning("Could not find pyasn1 and pyasn1_modules. " + \
"SSL certificate COULD NOT BE VERIFIED.")
return
not_before, not_after = extract_dates(raw_cert)
cert_names = extract_names(raw_cert)
now = datetime.utcnow()
if not_before > now:
raise CertificateError(
'Certificate has not entered its valid date range.')
if not_after <= now:
raise CertificateError(
'Certificate has expired.')
if '.' in expected:
expected_wild = expected[expected.index('.'):]
else:
expected_wild = expected
expected_srv = '_xmpp-client.%s' % expected
for name in cert_names['XMPPAddr']:
if name == expected:
return True
for name in cert_names['SRV']:
if name == expected_srv or name == expected:
return True
for name in cert_names['DNS']:
if name == expected:
return True
if name.startswith('*'):
if '.' in name:
name_wild = name[name.index('.'):]
else:
name_wild = name
if expected_wild == name_wild:
return True
for name in cert_names['URI']:
if name == expected:
return True
for name in cert_names['CN']:
if name == expected:
return True
raise CertificateError(
'Could not match certificate against hostname: %s' % expected)
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment