#!/usr/bin/env python2.7 # # ---------------------------------------------------------------------------- # "THE BEER-WARE LICENSE" (Revision 42): # wrote this file. As long as you retain this notice you # can do whatever you want with this stuff. If we meet some day, and you think # this stuff is worth it, you can buy me a beer in return. # ---------------------------------------------------------------------------- # SSL_RENEW_MANUAL="https://docs.google.com/a/adaptainc.com/document/d/1Ydz1bD9s5GcjfZSB-YPUuYIg3tDczVLrA6jip-kOPx0/preview" SSL_EXPIRE_WARN_DAYS = 15 import sys if len(sys.argv) < 2: print "Usage: %s hostname1 [hostname2] [hostname3] ..." % sys.argv[0] print print "Preparation:" print " $ virtualenv venv" print " $ . venv/bin/activate" print " $ pip install pytz pyasn1 pyOpenSSL ndg-httpsclient" import ssl from datetime import datetime from datetime import timedelta import pytz import OpenSSL import socket import re from ndg.httpsclient.subj_alt_name import SubjectAltName from pyasn1.codec.der import decoder as der_decoder import pyasn1 from lib.youtrack_connector import createIssue def _dnsname_match(dn, hostname, max_wildcards=1): """Matching according to RFC 6125, section 6.4.3 http://tools.ietf.org/html/rfc6125#section-6.4.3 """ pats = [] if not dn: return False pieces = dn.split(r'.') leftmost = pieces[0] remainder = pieces[1:] wildcards = leftmost.count('*') if wildcards > max_wildcards: # Issue #17980: avoid denials of service by refusing more # than one wildcard per fragment. A survery of established # policy among SSL implementations showed it to be a # reasonable choice. raise CertificateError( "too many wildcards in certificate DNS name: " + repr(dn)) # speed up common case w/o wildcards if not wildcards: return dn.lower() == hostname.lower() # RFC 6125, section 6.4.3, subitem 1. # The client SHOULD NOT attempt to match a presented identifier in which # the wildcard character comprises a label other than the left-most label. if leftmost == '*': # When '*' is a fragment by itself, it matches a non-empty dotless # fragment. pats.append('[^.]+') elif leftmost.startswith('xn--') or hostname.startswith('xn--'): # RFC 6125, section 6.4.3, subitem 3. # The client SHOULD NOT attempt to match a presented identifier # where the wildcard character is embedded within an A-label or # U-label of an internationalized domain name. pats.append(re.escape(leftmost)) else: # Otherwise, '*' matches any dotless string, e.g. www* pats.append(re.escape(leftmost).replace(r'\*', '[^.]*')) # add the remaining fragments, ignore any wildcards for frag in remainder: pats.append(re.escape(frag)) pat = re.compile(r'\A' + r'\.'.join(pats) + r'\Z', re.IGNORECASE) return True if (pat.match(hostname)) else False; def get_subj_alt_name(peer_cert): ''' Copied from ndg.httpsclient.ssl_peer_verification.ServerSSLCertVerification Extract subjectAltName DNS name settings from certificate extensions @param peer_cert: peer certificate in SSL connection. subjectAltName settings if any will be extracted from this @type peer_cert: OpenSSL.crypto.X509 ''' # Search through extensions dns_name = [] general_names = SubjectAltName() for i in range(peer_cert.get_extension_count()): ext = peer_cert.get_extension(i) ext_name = ext.get_short_name() if ext_name == "subjectAltName": # PyOpenSSL returns extension data in ASN.1 encoded form ext_dat = ext.get_data() decoded_dat = der_decoder.decode(ext_dat, asn1Spec=general_names) for name in decoded_dat: if isinstance(name, SubjectAltName): for entry in range(len(name)): component = name.getComponentByPosition(entry) dns_name.append(str(component.getComponent())) return dns_name color = { False: "\033[31;1m", True: "\033[32;1m", 'end': "\033[0m", 'error': "\033[33;1m", } okError = { False: "!!! ", True: " " } for server in sys.argv[1:]: print "\nChecking certificate for %s" % server taskSubj = "" taskMsg = "" serverInfo = server ctx = OpenSSL.SSL.Context(ssl.PROTOCOL_TLSv1) s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) x509 = None try: s.connect((server, 443)) cnx = OpenSSL.SSL.Connection(ctx, s) cnx.set_tlsext_host_name(server) cnx.set_connect_state() cnx.do_handshake() x509 = cnx.get_peer_certificate() s.close() issuer = x509.get_issuer() issuer_corp = x509.get_issuer().organizationName issuer_url = x509.get_issuer().organizationalUnitName issuer_x509 = x509.get_issuer().commonName server_name = x509.get_subject().commonName server_name_ok = _dnsname_match(server_name, server) try: subjectAltNames = get_subj_alt_name(x509) except pyasn1.error.PyAsn1Error: subjectAltNames = [] server_name_alt_ok = False; for an in subjectAltNames: if _dnsname_match(an, server): server_name_alt_ok = True server_name_alt = an break if server_name_alt_ok: server_name_alt_ok = True #server_name_alt = server elif len(subjectAltNames) == 0: server_name_alt = None else: server_name_alt = subjectAltNames[0] if len(subjectAltNames) > 1: server_name_alt += " (+%i)" % (len(subjectAltNames) - 1) now = datetime.now(pytz.utc) begin = datetime.strptime(x509.get_notBefore(), "%Y%m%d%H%M%SZ").replace(tzinfo=pytz.UTC) begin_ok = begin < now end = datetime.strptime(x509.get_notAfter(), "%Y%m%d%H%M%SZ").replace(tzinfo=pytz.UTC) end_ok = end > now expire_warn_date = end - timedelta(days=SSL_EXPIRE_WARN_DAYS) expire_ok = now < expire_warn_date expire_in_days = (end - now).days if not server_name_ok and not server_name_alt_ok: taskSubj = "Pay attention to certificate at %s" % server taskMsg = "Certificate do not match server name, please fix ASAP" elif not end_ok: taskSubj = "Immediately renew SSL certificate for %s" % server taskMsg = "https://%s certificate already expired !!!\n\nPlease action IMMEDIATELY !!!\n\n%s" % (server, SSL_RENEW_MANUAL) elif not expire_ok: taskSubj = "Renew SSL certificate for %s" % server taskMsg = "https://%s certificate will expire in %s days.\n\nPlease renew according to procedure described in %s" % (server, expire_in_days, SSL_RENEW_MANUAL) serverInfo = "%30s: %s%30s (alt: %s%30s) begin=%s%s end=%s%s expiration %s%s(%s) in days issuer=%s" % (server, okError[server_name_ok], server_name, okError[server_name_alt_ok], server_name_alt, okError[begin_ok], begin.strftime("%d.%m.%Y"), okError[end_ok], end.strftime("%d.%m.%Y"), okError[expire_ok], expire_in_days, SSL_EXPIRE_WARN_DAYS, issuer_corp) except Exception as e: taskSubj = "Checking %s certificate failed" % server taskMsg = "%30s: %s" % (server, e) if taskMsg: taskMsg += "\n\n" + serverInfo + "\n\nThank you in advance\nRobot ;)" print "Creating task --------------------" print taskSubj print taskMsg taskCreateResult = "created" try: issue = createIssue( project = "OFF", assignee = None, summary = taskSubj, description = taskMsg, subsystem = "Hosting", priority = "Critical" ) except Exception as e: taskCreateResult = "FAILED with exception: %s" % e print "Task %s ---------------------" % taskCreateResult else: print serverInfo print "Check passed for %s" % server