rewrote utilizing a sock-based github gist I found

This commit is contained in:
Alex Kelly 2021-09-30 12:36:53 -04:00
parent d2a3a505fb
commit 8ddf5f1b49
3 changed files with 144 additions and 29 deletions

View file

@ -1,10 +1,97 @@
import ssl # heavily modified version of https://gist.githubusercontent.com/gdamjan/55a8b9eec6cf7b771f92021d93b87b2c/raw/d8dc194ec4d0187f985a57138019d04e3a59b51f/ssl-check.py
import sys # to give a CLI for passing the hosts to check and other optional output
import click import click
import M2Crypto from OpenSSL import SSL
from OpenSSL import crypto
from cryptography import x509
from cryptography.x509.oid import NameOID
import idna
import sys
from socket import socket
from collections import namedtuple
__version__ = "0.1.0" __version__ = "0.1.0"
HostInfo = namedtuple(field_names="cert hostname peername", typename="HostInfo")
def verify_cert(cert, hostname):
# verify notAfter/notBefore, CA trusted, servername/sni/hostname
cert.has_expired()
# service_identity.pyopenssl.verify_hostname(client_ssl, hostname)
# issuer
def get_certificate(hostname, port):
hostname_idna = idna.encode(hostname)
sock = socket()
try:
sock.connect((hostname, port))
peername = sock.getpeername()
ctx = SSL.Context(SSL.SSLv23_METHOD) # most compatible
ctx.check_hostname = False
ctx.verify_mode = SSL.VERIFY_NONE
sock_ssl = SSL.Connection(ctx, sock)
sock_ssl.set_connect_state()
sock_ssl.set_tlsext_host_name(hostname_idna)
sock_ssl.do_handshake()
cert = sock_ssl.get_peer_certificate()
crypto_cert = cert.to_cryptography()
sock_ssl.close()
sock.close()
except ConnectionRefusedError:
pass
return HostInfo(cert=crypto_cert, peername=peername, hostname=hostname)
def get_alt_names(cert):
try:
ext = cert.extensions.get_extension_for_class(x509.SubjectAlternativeName)
return ext.value.get_values_for_type(x509.DNSName)
except x509.ExtensionNotFound:
return None
def get_x509_text(cert):
return crypto.dump_certificate(crypto.FILETYPE_TEXT, cert)
def get_common_name(cert):
try:
names = cert.subject.get_attributes_for_oid(NameOID.COMMON_NAME)
return names[0].value
except x509.ExtensionNotFound:
return None
def get_issuer(cert):
try:
names = cert.issuer.get_attributes_for_oid(NameOID.COMMON_NAME)
return names[0].value
except x509.ExtensionNotFound:
return None
def print_basic_info(hostinfo):
print(
f"""
{hostinfo.hostname} ({hostinfo.peername[0]}:{hostinfo.peername[1]})
\tcommonName: {get_common_name(hostinfo.cert)}
\tSAN: {get_alt_names(hostinfo.cert)}
\tissuer: {get_issuer(hostinfo.cert)}
\tnotBefore: {hostinfo.cert.not_valid_before}
\tnotAfter: {hostinfo.cert.not_valid_after}
"""
)
def check_it_out(hostname, port):
hostinfo = get_certificate(hostname, port)
print_basic_info(hostinfo)
@click.command() @click.command()
@click.version_option(__version__, prog_name="checkcert") @click.version_option(__version__, prog_name="checkcert")
@ -12,29 +99,36 @@ __version__ = "0.1.0"
@click.option( @click.option(
"--dump", is_flag=True, help="Dump the full text version of the x509 certificate" "--dump", is_flag=True, help="Dump the full text version of the x509 certificate"
) )
@click.option(
"--port", default=443, type=int, help="TCP port to connect to (default 443)"
)
@click.option("--expires", is_flag=True, help="Display the expiration date") @click.option("--expires", is_flag=True, help="Display the expiration date")
@click.argument("domain") @click.argument("hosts", nargs=-1)
def main(san, dump, port, expires, domain): def main(san, dump, expires, hosts):
# setup the list of tuples
HOSTS = []
# handle a domain given with a : in it to specify the port # handle a domain given with a : in it to specify the port
if ":" in domain: for host in hosts:
uri = domain.split(":") # if a host has a : in it, split on the :, first field will be host
domain = uri[0] # second field will be the port
port = uri[1] if ":" in host:
cert = ssl.get_server_certificate((domain, port)) host_info = host.split(":")
x509 = M2Crypto.X509.load_cert_string(cert) HOSTS.append((host_info[0], int(host_info[1])))
else:
HOSTS.append((host, 443))
output_string = ""
for hostinfo in map(lambda x: get_certificate(x[0], x[1]), HOSTS):
if dump: if dump:
print(x509.as_text()) print(get_x509_text(hostinfo.cert).decode())
sys.exit() else:
output_string += (
f"{hostinfo.hostname} ({hostinfo.peername[0]}:{hostinfo.peername[1]})\n"
)
output_string += f"\tcommonName: {get_common_name(hostinfo.cert)}\n"
if san: if san:
all_sans = x509.get_ext("subjectAltName").get_value() output_string += f"\tSAN: {get_alt_names(hostinfo.cert)}\n"
sans = all_sans.split(",") output_string += f"\tissuer: {get_issuer(hostinfo.cert)}\n"
for san in sans: output_string += f"\tnotBefore: {hostinfo.cert.not_valid_before}\n"
print(str(san).strip().removeprefix("DNS:")) output_string += f"\tnotAfter: {hostinfo.cert.not_valid_after}\n\n"
print(x509.get_subject().as_text()) print(output_string)
print(f"Certificate for {domain}\nexpires after: {x509.get_not_after()}") # print(f"Certificate for {domain}\nexpires after: {x509.get_not_after()}")
if __name__ == "__main__": if __name__ == "__main__":

30
poetry.lock generated
View file

@ -58,7 +58,7 @@ python-versions = "*"
name = "cffi" name = "cffi"
version = "1.14.6" version = "1.14.6"
description = "Foreign Function Interface for Python calling C code." description = "Foreign Function Interface for Python calling C code."
category = "dev" category = "main"
optional = false optional = false
python-versions = "*" python-versions = "*"
@ -121,7 +121,7 @@ toml = ["toml"]
name = "cryptography" name = "cryptography"
version = "3.4.8" version = "3.4.8"
description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers."
category = "dev" category = "main"
optional = false optional = false
python-versions = ">=3.6" python-versions = ">=3.6"
@ -336,7 +336,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
name = "pycparser" name = "pycparser"
version = "2.20" version = "2.20"
description = "C parser in Python" description = "C parser in Python"
category = "dev" category = "main"
optional = false optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
@ -373,6 +373,22 @@ platformdirs = ">=2.2.0"
toml = ">=0.7.1" toml = ">=0.7.1"
typing-extensions = {version = ">=3.10.0", markers = "python_version < \"3.10\""} typing-extensions = {version = ">=3.10.0", markers = "python_version < \"3.10\""}
[[package]]
name = "pyopenssl"
version = "21.0.0"
description = "Python wrapper module around the OpenSSL library"
category = "main"
optional = false
python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*"
[package.dependencies]
cryptography = ">=3.3"
six = ">=1.5.2"
[package.extras]
docs = ["sphinx", "sphinx-rtd-theme"]
test = ["flaky", "pretend", "pytest (>=3.0.1)"]
[[package]] [[package]]
name = "pyparsing" name = "pyparsing"
version = "2.4.7" version = "2.4.7"
@ -548,7 +564,7 @@ toml = ["setuptools (>=42)", "tomli (>=1.0.0)"]
name = "six" name = "six"
version = "1.16.0" version = "1.16.0"
description = "Python 2 and 3 compatibility utilities" description = "Python 2 and 3 compatibility utilities"
category = "dev" category = "main"
optional = false optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*"
@ -671,7 +687,7 @@ testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytes
[metadata] [metadata]
lock-version = "1.1" lock-version = "1.1"
python-versions = "^3.9" python-versions = "^3.9"
content-hash = "c1ffdd5000aa0956b993deef3eefa5f636b041a5c5a4a35b56c638d397f82bbf" content-hash = "f9e3c5502f1aa3c91b8c2a76f7f4c14d39cb84281143d987cf9e8ed043efa175"
[metadata.files] [metadata.files]
astroid = [ astroid = [
@ -938,6 +954,10 @@ pylint = [
{file = "pylint-2.11.1-py3-none-any.whl", hash = "sha256:0f358e221c45cbd4dad2a1e4b883e75d28acdcccd29d40c76eb72b307269b126"}, {file = "pylint-2.11.1-py3-none-any.whl", hash = "sha256:0f358e221c45cbd4dad2a1e4b883e75d28acdcccd29d40c76eb72b307269b126"},
{file = "pylint-2.11.1.tar.gz", hash = "sha256:2c9843fff1a88ca0ad98a256806c82c5a8f86086e7ccbdb93297d86c3f90c436"}, {file = "pylint-2.11.1.tar.gz", hash = "sha256:2c9843fff1a88ca0ad98a256806c82c5a8f86086e7ccbdb93297d86c3f90c436"},
] ]
pyopenssl = [
{file = "pyOpenSSL-21.0.0-py2.py3-none-any.whl", hash = "sha256:8935bd4920ab9abfebb07c41a4f58296407ed77f04bd1a92914044b848ba1ed6"},
{file = "pyOpenSSL-21.0.0.tar.gz", hash = "sha256:5e2d8c5e46d0d865ae933bef5230090bdaf5506281e9eec60fa250ee80600cb3"},
]
pyparsing = [ pyparsing = [
{file = "pyparsing-2.4.7-py2.py3-none-any.whl", hash = "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b"}, {file = "pyparsing-2.4.7-py2.py3-none-any.whl", hash = "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b"},
{file = "pyparsing-2.4.7.tar.gz", hash = "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1"}, {file = "pyparsing-2.4.7.tar.gz", hash = "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1"},

View file

@ -7,6 +7,7 @@ authors = ["Alex Kelly <alex.kelly@franklin.edu>"]
[tool.poetry.dependencies] [tool.poetry.dependencies]
python = "^3.9" python = "^3.9"
click = "^8.0.1" click = "^8.0.1"
pyOpenSSL = "^21.0.0"
[tool.poetry.dev-dependencies] [tool.poetry.dev-dependencies]
python-semantic-release = "^7.19.2" python-semantic-release = "^7.19.2"