rewrote utilizing a sock-based github gist I found
This commit is contained in:
parent
d2a3a505fb
commit
8ddf5f1b49
3 changed files with 144 additions and 29 deletions
|
@ -1,10 +1,97 @@
|
|||
import ssl
|
||||
import sys
|
||||
# heavily modified version of https://gist.githubusercontent.com/gdamjan/55a8b9eec6cf7b771f92021d93b87b2c/raw/d8dc194ec4d0187f985a57138019d04e3a59b51f/ssl-check.py
|
||||
# to give a CLI for passing the hosts to check and other optional output
|
||||
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"
|
||||
|
||||
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.version_option(__version__, prog_name="checkcert")
|
||||
|
@ -12,29 +99,36 @@ __version__ = "0.1.0"
|
|||
@click.option(
|
||||
"--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.argument("domain")
|
||||
def main(san, dump, port, expires, domain):
|
||||
@click.argument("hosts", nargs=-1)
|
||||
def main(san, dump, expires, hosts):
|
||||
# setup the list of tuples
|
||||
HOSTS = []
|
||||
# handle a domain given with a : in it to specify the port
|
||||
if ":" in domain:
|
||||
uri = domain.split(":")
|
||||
domain = uri[0]
|
||||
port = uri[1]
|
||||
cert = ssl.get_server_certificate((domain, port))
|
||||
x509 = M2Crypto.X509.load_cert_string(cert)
|
||||
if dump:
|
||||
print(x509.as_text())
|
||||
sys.exit()
|
||||
if san:
|
||||
all_sans = x509.get_ext("subjectAltName").get_value()
|
||||
sans = all_sans.split(",")
|
||||
for san in sans:
|
||||
print(str(san).strip().removeprefix("DNS:"))
|
||||
print(x509.get_subject().as_text())
|
||||
print(f"Certificate for {domain}\nexpires after: {x509.get_not_after()}")
|
||||
for host in hosts:
|
||||
# if a host has a : in it, split on the :, first field will be host
|
||||
# second field will be the port
|
||||
if ":" in host:
|
||||
host_info = host.split(":")
|
||||
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:
|
||||
print(get_x509_text(hostinfo.cert).decode())
|
||||
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:
|
||||
output_string += f"\tSAN: {get_alt_names(hostinfo.cert)}\n"
|
||||
output_string += f"\tissuer: {get_issuer(hostinfo.cert)}\n"
|
||||
output_string += f"\tnotBefore: {hostinfo.cert.not_valid_before}\n"
|
||||
output_string += f"\tnotAfter: {hostinfo.cert.not_valid_after}\n\n"
|
||||
print(output_string)
|
||||
# print(f"Certificate for {domain}\nexpires after: {x509.get_not_after()}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
|
30
poetry.lock
generated
30
poetry.lock
generated
|
@ -58,7 +58,7 @@ python-versions = "*"
|
|||
name = "cffi"
|
||||
version = "1.14.6"
|
||||
description = "Foreign Function Interface for Python calling C code."
|
||||
category = "dev"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
|
||||
|
@ -121,7 +121,7 @@ toml = ["toml"]
|
|||
name = "cryptography"
|
||||
version = "3.4.8"
|
||||
description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers."
|
||||
category = "dev"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.6"
|
||||
|
||||
|
@ -336,7 +336,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
|
|||
name = "pycparser"
|
||||
version = "2.20"
|
||||
description = "C parser in Python"
|
||||
category = "dev"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
|
||||
|
||||
|
@ -373,6 +373,22 @@ platformdirs = ">=2.2.0"
|
|||
toml = ">=0.7.1"
|
||||
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]]
|
||||
name = "pyparsing"
|
||||
version = "2.4.7"
|
||||
|
@ -548,7 +564,7 @@ toml = ["setuptools (>=42)", "tomli (>=1.0.0)"]
|
|||
name = "six"
|
||||
version = "1.16.0"
|
||||
description = "Python 2 and 3 compatibility utilities"
|
||||
category = "dev"
|
||||
category = "main"
|
||||
optional = false
|
||||
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]
|
||||
lock-version = "1.1"
|
||||
python-versions = "^3.9"
|
||||
content-hash = "c1ffdd5000aa0956b993deef3eefa5f636b041a5c5a4a35b56c638d397f82bbf"
|
||||
content-hash = "f9e3c5502f1aa3c91b8c2a76f7f4c14d39cb84281143d987cf9e8ed043efa175"
|
||||
|
||||
[metadata.files]
|
||||
astroid = [
|
||||
|
@ -938,6 +954,10 @@ pylint = [
|
|||
{file = "pylint-2.11.1-py3-none-any.whl", hash = "sha256:0f358e221c45cbd4dad2a1e4b883e75d28acdcccd29d40c76eb72b307269b126"},
|
||||
{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 = [
|
||||
{file = "pyparsing-2.4.7-py2.py3-none-any.whl", hash = "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b"},
|
||||
{file = "pyparsing-2.4.7.tar.gz", hash = "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1"},
|
||||
|
|
|
@ -7,6 +7,7 @@ authors = ["Alex Kelly <alex.kelly@franklin.edu>"]
|
|||
[tool.poetry.dependencies]
|
||||
python = "^3.9"
|
||||
click = "^8.0.1"
|
||||
pyOpenSSL = "^21.0.0"
|
||||
|
||||
[tool.poetry.dev-dependencies]
|
||||
python-semantic-release = "^7.19.2"
|
||||
|
|
Loading…
Reference in a new issue