-
-
Save crashdump/5683952 to your computer and use it in GitHub Desktop.
#!/usr/bin/env python | |
# -*- coding: utf-8 -*- | |
__author__ = "Adrien Pujol - http://www.crashdump.fr/" | |
__copyright__ = "Copyright 2013, Adrien Pujol" | |
__license__ = "Mozilla Public License" | |
__version__ = "0.3" | |
__email__ = "[email protected]" | |
__status__ = "Development" | |
__doc__ = "Check a TLS certificate validity." | |
import argparse | |
import socket | |
from datetime import datetime | |
import time | |
try: | |
# Try to load pyOpenSSL first | |
# aptitude install python-dev && pip install pyopenssl | |
from OpenSSL import SSL | |
PYOPENSSL = True | |
except ImportError: | |
# Else, fallback on standard ssl lib (doesn't support SNI) | |
import ssl | |
PYOPENSSL = False | |
CA_CERTS = "/etc/ssl/certs/ca-certificates.crt" | |
def exit_error(errcode, errtext): | |
print errtext | |
exit(errcode) | |
def pyopenssl_check_callback(connection, x509, errnum, errdepth, ok): | |
''' callback for pyopenssl ssl check''' | |
if x509.get_subject().commonName == HOST: | |
if x509.has_expired(): | |
exit_error(1, 'Error: Certificate has expired!') | |
else: | |
print pyopenssl_check_expiration(x509.get_notAfter()) | |
if not ok: | |
return False | |
return ok | |
def pyopenssl_check_expiration(asn1): | |
''' Return the numbers of day before expiration. False if expired.''' | |
try: | |
expire_date = datetime.strptime(asn1, "%Y%m%d%H%M%SZ") | |
except: | |
exit_error(1, 'Certificate date format unknow.') | |
expire_in = expire_date - datetime.now() | |
if expire_in.days > 0: | |
return expire_in.days | |
else: | |
return False | |
def pyssl_check_hostname(cert, hostname): | |
''' Return True if valid. False is invalid ''' | |
if 'subjectAltName' in cert: | |
for typ, val in cert['subjectAltName']: | |
# Wilcard | |
if typ == 'DNS' and val.startswith('*'): | |
if val[2:] == hostname.split('.', 1)[1]: | |
return True | |
# Normal hostnames | |
elif typ == 'DNS' and val == hostname: | |
return True | |
else: | |
return False | |
def pyssl_check_expiration(cert): | |
''' Return the numbers of day before expiration. False if expired. ''' | |
if 'notAfter' in cert: | |
try: | |
expire_date = datetime.strptime(cert['notAfter'], | |
"%b %d %H:%M:%S %Y %Z") | |
except: | |
exit_error(1, 'Certificate date format unknow.') | |
expire_in = expire_date - datetime.now() | |
if expire_in.days > 0: | |
return expire_in.days | |
else: | |
return False | |
def main(): | |
parser = argparse.ArgumentParser() | |
parser.add_argument('host', help='specify an host to connect to') | |
parser.add_argument('-p', '--port', help='specify a port to connect to', | |
type=int, default=443) | |
args = parser.parse_args() | |
global HOST, PORT | |
HOST = args.host | |
PORT = args.port | |
# Check the DNS name | |
try: | |
socket.getaddrinfo(HOST, PORT)[0][4][0] | |
except socket.gaierror as e: | |
exit_error(1, e) | |
# Connect to the host and get the certificate | |
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) | |
sock.connect((HOST, PORT)) | |
# If handled by python SSL library | |
if not PYOPENSSL: | |
try: | |
ssl_sock = ssl.wrap_socket(sock, cert_reqs=ssl.CERT_REQUIRED, | |
ca_certs=CA_CERTS, | |
ciphers=("HIGH:-aNULL:-eNULL:" | |
"-PSK:RC4-SHA:RC4-MD5")) | |
cert = ssl_sock.getpeercert() | |
if not pyssl_check_hostname(cert, HOST): | |
print 'Error: Hostname does not match!' | |
print pyssl_check_expiration(cert) | |
sock = ssl_sock.unwrap() | |
except ssl.SSLError as e: | |
exit_error(1, e) | |
# If handled by pyOpenSSL module | |
else: | |
try: | |
ctx = SSL.Context(SSL.TLSv1_METHOD) | |
ctx.set_verify(SSL.VERIFY_PEER | SSL.VERIFY_FAIL_IF_NO_PEER_CERT, | |
pyopenssl_check_callback) | |
ctx.load_verify_locations(CA_CERTS) | |
ssl_sock = SSL.Connection(ctx, sock) | |
ssl_sock.set_connect_state() | |
ssl_sock.set_tlsext_host_name(HOST) | |
ssl_sock.do_handshake() | |
x509 = ssl_sock.get_peer_certificate() | |
x509name = x509.get_subject() | |
if x509name.commonName != HOST: | |
print 'Error: Hostname does not match!' | |
ssl_sock.shutdown() | |
except SSL.Error as e: | |
exit_error(1, e) | |
sock.close() | |
if __name__ == "__main__": | |
main() |
<?xml version="1.0" encoding="UTF-8"?> | |
<zabbix_export> | |
<version>2.0</version> | |
<date>2013-11-15T12:28:00Z</date> | |
<groups> | |
<group> | |
<name>Templates</name> | |
</group> | |
</groups> | |
<templates> | |
<template> | |
<template>Template External Check - SSL Cert Expire</template> | |
<name>Template External Check - SSL Cert Expire</name> | |
<groups> | |
<group> | |
<name>Templates</name> | |
</group> | |
</groups> | |
<applications> | |
<application> | |
<name>SSL certificate</name> | |
</application> | |
</applications> | |
<items> | |
<item> | |
<name>SSL certificate validity</name> | |
<type>10</type> | |
<snmp_community/> | |
<multiplier>0</multiplier> | |
<snmp_oid/> | |
<key>check-ssl-expire.py["{HOST.NAME}"]</key> | |
<delay>86400</delay> | |
<history>14</history> | |
<trends>365</trends> | |
<status>0</status> | |
<value_type>0</value_type> | |
<allowed_hosts/> | |
<units>days</units> | |
<delta>0</delta> | |
<snmpv3_contextname/> | |
<snmpv3_securityname/> | |
<snmpv3_securitylevel>0</snmpv3_securitylevel> | |
<snmpv3_authprotocol>0</snmpv3_authprotocol> | |
<snmpv3_authpassphrase/> | |
<snmpv3_privprotocol>0</snmpv3_privprotocol> | |
<snmpv3_privpassphrase/> | |
<formula>1</formula> | |
<delay_flex/> | |
<params/> | |
<ipmi_sensor/> | |
<data_type>0</data_type> | |
<authtype>0</authtype> | |
<username/> | |
<password/> | |
<publickey/> | |
<privatekey/> | |
<port/> | |
<description/> | |
<inventory_link>0</inventory_link> | |
<applications> | |
<application> | |
<name>SSL certificate</name> | |
</application> | |
</applications> | |
<valuemap/> | |
</item> | |
</items> | |
<discovery_rules/> | |
<macros> | |
<macro> | |
<macro>{$SSL_PORT}</macro> | |
<value>443</value> | |
</macro> | |
</macros> | |
<templates/> | |
<screens/> | |
</template> | |
</templates> | |
<triggers> | |
<trigger> | |
<expression>{Template External Check - SSL Cert Expire:check-ssl-expire.py["{HOST.NAME}"].last(0)}<1</expression> | |
<name>SSL certificate on {HOSTNAME} expired</name> | |
<url/> | |
<status>0</status> | |
<priority>5</priority> | |
<description/> | |
<type>0</type> | |
<dependencies/> | |
</trigger> | |
<trigger> | |
<expression>{Template External Check - SSL Cert Expire:check-ssl-expire.py["{HOST.NAME}"].last(0)}<7</expression> | |
<name>SSL certificate on {HOSTNAME} expires in less than 7 days ({ITEM.VALUE} days remaining)</name> | |
<url/> | |
<status>0</status> | |
<priority>4</priority> | |
<description/> | |
<type>0</type> | |
<dependencies> | |
<dependency> | |
<name>SSL certificate on {HOSTNAME} expired</name> | |
<expression>{Template External Check - SSL Cert Expire:check-ssl-expire.py["{HOST.NAME}"].last(0)}<1</expression> | |
</dependency> | |
</dependencies> | |
</trigger> | |
<trigger> | |
<expression>{Template External Check - SSL Cert Expire:check-ssl-expire.py["{HOST.NAME}"].last(0)}<15</expression> | |
<name>SSL certificate on {HOSTNAME} expires in less than 15 days ({ITEM.VALUE} days remaining)</name> | |
<url/> | |
<status>0</status> | |
<priority>3</priority> | |
<description/> | |
<type>0</type> | |
<dependencies> | |
<dependency> | |
<name>SSL certificate on {HOSTNAME} expires in less than 7 days ({ITEM.VALUE} days remaining)</name> | |
<expression>{Template External Check - SSL Cert Expire:check-ssl-expire.py["{HOST.NAME}"].last(0)}<7</expression> | |
</dependency> | |
</dependencies> | |
</trigger> | |
<trigger> | |
<expression>{Template External Check - SSL Cert Expire:check-ssl-expire.py["{HOST.NAME}"].last(0)}<30</expression> | |
<name>SSL certificate on {HOSTNAME} expires in less than 30 days ({ITEM.VALUE} days remaining)</name> | |
<url/> | |
<status>0</status> | |
<priority>2</priority> | |
<description/> | |
<type>0</type> | |
<dependencies> | |
<dependency> | |
<name>SSL certificate on {HOSTNAME} expires in less than 15 days ({ITEM.VALUE} days remaining)</name> | |
<expression>{Template External Check - SSL Cert Expire:check-ssl-expire.py["{HOST.NAME}"].last(0)}<15</expression> | |
</dependency> | |
</dependencies> | |
</trigger> | |
<trigger> | |
<expression>{Template External Check - SSL Cert Expire:check-ssl-expire.py["{HOST.NAME}"].last(0)}<60</expression> | |
<name>SSL certificate on {HOSTNAME} expires in less than 60 days ({ITEM.VALUE} days remaining)</name> | |
<url/> | |
<status>0</status> | |
<priority>1</priority> | |
<description/> | |
<type>0</type> | |
<dependencies> | |
<dependency> | |
<name>SSL certificate on {HOSTNAME} expires in less than 30 days ({ITEM.VALUE} days remaining)</name> | |
<expression>{Template External Check - SSL Cert Expire:check-ssl-expire.py["{HOST.NAME}"].last(0)}<30</expression> | |
</dependency> | |
</dependencies> | |
</trigger> | |
<trigger> | |
<expression>{Template External Check - SSL Cert Expire:check-ssl-expire.py["{HOST.NAME}"].last(0)}<90</expression> | |
<name>SSL certificate on {HOSTNAME} expires in less than 90 days ({ITEM.VALUE} days remaining)</name> | |
<url/> | |
<status>0</status> | |
<priority>0</priority> | |
<description/> | |
<type>0</type> | |
<dependencies> | |
<dependency> | |
<name>SSL certificate on {HOSTNAME} expires in less than 60 days ({ITEM.VALUE} days remaining)</name> | |
<expression>{Template External Check - SSL Cert Expire:check-ssl-expire.py["{HOST.NAME}"].last(0)}<60</expression> | |
</dependency> | |
</dependencies> | |
</trigger> | |
<trigger> | |
<expression>{Template External Check - SSL Cert Expire:check-ssl-expire.py["{HOST.NAME}"].nodata(86500)}=1</expression> | |
<name>SSL certificate on {HOSTNAME} No new data in the last 24h</name> | |
<url/> | |
<status>0</status> | |
<priority>2</priority> | |
<description>No data received for > 24h. Check the script.</description> | |
<type>0</type> | |
<dependencies/> | |
</trigger> | |
</triggers> | |
</zabbix_export> |
AttributeError: '_socketobject' object has no attribute 'set_tlsext_host_name'
That happend because Ubuntu 12.04 (that is my server's OS) has old pyOpenSSL library which not accept attribute 'set_tlsext_host_name'.
For fix that, you need to add dependence pyOpenSSL >= 0.13.
On Ubuntu for update pyOpenSSL use pip, you also need to install libffi-dev and remove python-openssl by apt.
Add that to instruction.
FYI This also does not handle wildcard certs. x509name.commonName won't match HOST.
Hey @crashdump, congrats on your work.
I've added a version check on my fork. https://gist.github.com/vchoi/f374b3d4fceb1b975b98
I couldn't find any way to make a pull request. Have you considered moving this gist into a github project?
Wildcards check still not work!
Hi @crashdump, thank you for providing this python script. We also have problems regarding wildcard certs. They always result in an error, even if they're valid. Do you have a version where the wildcard certs work?
I'll try to modify this to get the wildcard working, will edit this post if I can figure it out.
Well, I tried to wrestle with pyOpenSSL library all day and didn't have much luck. The good news is though I was able to rewrite the whole thing in Ruby and add Wildcard SSL and SNI support and did away with the hard coded CA cert path (ruby is able to detect it on its own and I added an option if you want to pass a filepath in). Let me know what you think: https://gist.github.com/jbsmith86/df7d9aad39e5acf23f2caf73d17b1796
Hello i got the following error:
./ssl-exp.py <SERVER_FQDN>
1137
Traceback (most recent call last):
File "./ssl-exp.py", line 158, in <module>
main()
File "./ssl-exp.py", line 126, in main
sock = ssl_sock.unwrap()
File "/usr/lib/python2.7/ssl.py", line 823, in unwrap
s = self._sslobj.shutdown()
socket.error: [Errno 0] Error
Hello i got the following error:
./ssl-exp.py <SERVER_FQDN>
1137
Traceback (most recent call last):
File "./ssl-exp.py", line 158, in
main()
File "./ssl-exp.py", line 126, in main
sock = ssl_sock.unwrap()
File "/usr/lib/python2.7/ssl.py", line 823, in unwrap
s = self._sslobj.shutdown()
socket.error: [Errno 0] Error
Try to install "python-pip" and you'll see that it works!
Nice write-up: http://www.zabbix.org/wiki/Docs/howto/ssl_certificate_check