adding ascii ref links and ssl_tls/certparser.py
(because jthan keeps forgetting how to use openssl cli)
This commit is contained in:
parent
eb33ecd559
commit
36c20eae91
2
ref/ascii
Normal file
2
ref/ascii
Normal file
@ -0,0 +1,2 @@
|
||||
http://plato.asu.edu/MAT420/beginning_perl/3145_AppF.pdf
|
||||
http://www.profdavis.net/ASCII_table.pdf
|
450
ssl_tls/certparser.py
Executable file
450
ssl_tls/certparser.py
Executable file
@ -0,0 +1,450 @@
|
||||
#!/usr/bin/env python3.6
|
||||
|
||||
# stdlib
|
||||
import argparse
|
||||
import collections
|
||||
import copy
|
||||
import datetime
|
||||
import hashlib
|
||||
import importlib
|
||||
import ipaddress
|
||||
import json
|
||||
import os
|
||||
import pprint
|
||||
import re
|
||||
import shutil
|
||||
import socket
|
||||
import ssl
|
||||
from urllib import parse
|
||||
# PyPi/PIP
|
||||
# These are handled automagically.
|
||||
# If you'd rather install them via your distro's package manager (YOU SHOULD),
|
||||
# then install them first then run this script.
|
||||
# Otherwise you'll have to use pip to remove them.
|
||||
thrd_prty = {'OpenSSL': 'pyOpenSSL',
|
||||
#'pyasn1': 'pyasn1',
|
||||
#'jinja2': 'Jinja2',
|
||||
'validators': 'validators'}
|
||||
|
||||
cols = shutil.get_terminal_size((80, 20)).columns
|
||||
|
||||
for mod in thrd_prty:
|
||||
try:
|
||||
globals()[mod] = importlib.import_module(mod)
|
||||
except ImportError:
|
||||
import pip
|
||||
pip.main(['install', '--quiet', '--quiet', '--quiet',
|
||||
'--user', thrd_prty[mod]])
|
||||
globals()[mod] = importlib.import_module(mod)
|
||||
|
||||
class CertParse(object):
|
||||
def __init__(self, target, port = 443, force = None, cert_type = 'pem',
|
||||
json_fmt = False, starttls = False, extensions = False,
|
||||
alt_names = False):
|
||||
self.target = target
|
||||
self.port = port
|
||||
self.force_type = force
|
||||
self.cert_type = cert_type
|
||||
self.starttls = starttls
|
||||
self.json_fmt = json_fmt
|
||||
self.extensions = extensions
|
||||
self.alt_names = alt_names
|
||||
self.cert = None
|
||||
self.certinfo = None
|
||||
self.get_type()
|
||||
|
||||
def getCert(self):
|
||||
if self.cert_type.lower() == 'pem':
|
||||
self.cert_type = OpenSSL.crypto.FILETYPE_PEM
|
||||
elif self.cert_type.lower() == 'asn1':
|
||||
self.cert_type = OpenSSL.crypto.FILETYPE_ASN1
|
||||
else:
|
||||
raise ValueError(('{0} is not a valid cert type; must be either ' +
|
||||
'"pem" or "asn1"').format(self.cert_type))
|
||||
if not self.force_type in ('url', 'domain', 'ip'):
|
||||
with open(self.target, 'rb') as f:
|
||||
self.pkcs = OpenSSL.crypto.load_certificate(self.cert_type,
|
||||
f.read())
|
||||
else:
|
||||
_cert = ssl.get_server_certificate((self.target, self.port))
|
||||
self.pkcs = OpenSSL.crypto.load_certificate(self.cert_type,
|
||||
_cert)
|
||||
return()
|
||||
|
||||
def parseCert(self):
|
||||
certinfo = collections.OrderedDict()
|
||||
timefmt = '%Y%m%d%H%M%SZ'
|
||||
certinfo['Subject'] = self.parse_name(self.pkcs.get_subject().\
|
||||
get_components())
|
||||
certinfo['EXPIRED'] = self.pkcs.has_expired()
|
||||
certinfo['Issuer'] = self.parse_name(self.pkcs.get_issuer().\
|
||||
get_components())
|
||||
certinfo['Issued'] = str(datetime.datetime.strptime(
|
||||
self.pkcs.get_notBefore().decode('utf-8'),
|
||||
timefmt))
|
||||
certinfo['Expires'] = str(datetime.datetime.strptime(
|
||||
self.pkcs.get_notAfter().decode('utf-8'),
|
||||
timefmt))
|
||||
if self.extensions:
|
||||
certinfo['Extensions'] = self.parse_ext()
|
||||
elif self.alt_names:
|
||||
certinfo['SANs'] = self.parse_ext_san_only()
|
||||
# TODO: parse?
|
||||
#certinfo['Pubkey'] = self.pkcs.get_pubkey()
|
||||
certinfo['Serial'] = int(self.pkcs.get_serial_number())
|
||||
certinfo['Signature Algorithm'] = self.pkcs.get_signature_algorithm().\
|
||||
decode('utf-8')
|
||||
certinfo['Version'] = self.pkcs.get_version()
|
||||
certinfo['Subject Name Hash'] = self.pkcs.subject_name_hash()
|
||||
certinfo['Fingerprints'] = self.gen_hashes()
|
||||
self.certinfo = certinfo
|
||||
#print(certinfo)
|
||||
return()
|
||||
|
||||
def print(self, json_fmt = None):
|
||||
if json_fmt is None:
|
||||
json_fmt = self.json_fmt
|
||||
if json_fmt:
|
||||
output = json.dumps(self.certinfo, indent = 4)
|
||||
else:
|
||||
output = self.certinfo
|
||||
if __name__ == '__main__':
|
||||
if not json_fmt:
|
||||
pprint.pprint(output, compact = False, width = cols)
|
||||
else:
|
||||
print(output)
|
||||
return()
|
||||
return(output)
|
||||
|
||||
def gen_hashes(self):
|
||||
hashes = {}
|
||||
# Note: MD2 is *so old* that they aren't even
|
||||
# *supported in python 3*.
|
||||
# If we NEED to implement, https://urchin.earth.li/~twic/md2.py
|
||||
fpt_types = sorted([i.lower() for i in ['md2', 'md5', 'sha1', 'mdc2',
|
||||
'ripemd160', 'blake2b512',
|
||||
'blake2s256', 'sha224',
|
||||
'sha256', 'sha384', 'sha512']])
|
||||
supported_types = sorted([i.lower() for i in \
|
||||
list(hashlib.algorithms_available)])
|
||||
cert_hash_types = [i for i in fpt_types if i in supported_types]
|
||||
for h in cert_hash_types:
|
||||
hashes[h.upper()] = self.pkcs.digest(h).decode('utf-8')
|
||||
return(hashes)
|
||||
|
||||
def parse_name(self, item):
|
||||
component_map = {'C': 'Country',
|
||||
'countryName': 'Country',
|
||||
'ST': 'State/Province',
|
||||
'stateOrProvinceName': 'State/Province',
|
||||
'L': 'Locality/City/Town/Region',
|
||||
'localityName': 'Locality/City/Town/Region',
|
||||
'O': 'Organization',
|
||||
'organizationName': 'Organization',
|
||||
'OU': 'Department/Team/Organization Unit',
|
||||
'organizationalUnitName': ('Department/Team/' +
|
||||
'Organization Unit'),
|
||||
'CN': 'Common name',
|
||||
'commonName': 'Common name',
|
||||
'emailAddress': 'eMail Address'}
|
||||
info = {}
|
||||
for c in item:
|
||||
item = c[0].decode('utf-8')
|
||||
value = c[1].decode('utf-8')
|
||||
if item in component_map.keys():
|
||||
info[component_map[item]] = value
|
||||
else:
|
||||
info[item] = value
|
||||
return(info)
|
||||
|
||||
def parse_ext_san_only(self):
|
||||
SANs = []
|
||||
for idx in range(0, self.pkcs.get_extension_count()):
|
||||
ext = self.pkcs.get_extension(idx)
|
||||
name = ext.get_short_name().decode('utf-8').lower()
|
||||
x = str(ext).strip()
|
||||
if name == 'subjectaltname':
|
||||
val_lst = [i.strip() for i in x.split(',')]
|
||||
for v in val_lst:
|
||||
parsed_val = re.sub('^\s*DNS:\s*(.*)', '\g<1>', v)
|
||||
if parsed_val not in ('\n', ''):
|
||||
SANs.append(parsed_val.lower())
|
||||
return(SANs)
|
||||
|
||||
def parse_ext(self):
|
||||
exts = {}
|
||||
for idx in range(0, self.pkcs.get_extension_count()):
|
||||
ext = self.pkcs.get_extension(idx)
|
||||
keyname = ext.get_short_name().decode('utf-8')
|
||||
value_str = str(ext).strip()
|
||||
# These should be split into lists by commas.
|
||||
if keyname in ('subjectAltName', 'keyUsage', 'extendedKeyUsage',
|
||||
'basicConstraints'):
|
||||
val_lst = [i.strip() for i in value_str.split(',')]
|
||||
value_str = []
|
||||
for v in val_lst:
|
||||
parsed_val = re.sub('^\s*DNS:\s*(.*)', '\g<1>', v)
|
||||
if parsed_val not in ('\n', ''):
|
||||
value_str.append(parsed_val)
|
||||
# These should be split into lists by lines.
|
||||
elif keyname in ('certificatePolicies', 'ct_precert_scts',
|
||||
'authorityInfoAccess'):
|
||||
val_lst = [i.strip() for i in value_str.splitlines()]
|
||||
value_str = []
|
||||
for v in val_lst:
|
||||
value_str.append(v)
|
||||
exts[keyname] = value_str
|
||||
# These are split FURTHER into dicts but require unique... massaging.
|
||||
# authorityInfoAccess
|
||||
_tmp = copy.deepcopy(exts['authorityInfoAccess'])
|
||||
exts['authorityInfoAccess'] = {}
|
||||
for i in _tmp:
|
||||
x = [n.strip() for n in i.split('-', 1)]
|
||||
y = [n.strip() for n in x[1].split(':', 1)]
|
||||
exts['authorityInfoAccess'][x[0]] = {y[0]: y[1]}
|
||||
# authorityKeyIdentifier
|
||||
_tmp = copy.deepcopy(exts['authorityKeyIdentifier'])
|
||||
exts['authorityKeyIdentifier'] = {_tmp.split(':', 1)[0]:
|
||||
_tmp.split(':', 1)[1]}
|
||||
# basicConstraints
|
||||
_tmp = copy.deepcopy(exts['basicConstraints'])
|
||||
exts['basicConstraints'] = {}
|
||||
for i in _tmp:
|
||||
x = [n.strip() for n in i.split(':', 1)]
|
||||
if len(x) >= 1:
|
||||
if x[1].lower() in ('true', 'false'):
|
||||
x[1] = (x[1].lower() == 'true')
|
||||
exts['basicConstraints'][x[0]] = x[1]
|
||||
else:
|
||||
exts['basicConstraints'][x[0]] = True
|
||||
# certificatePolicies
|
||||
# What a mess.
|
||||
_tmp = copy.deepcopy(exts['certificatePolicies'])
|
||||
exts['certificatePolicies'] = {}
|
||||
last_key = None
|
||||
for i in [n.strip() for n in _tmp]:
|
||||
l = [y for y in i.split(':', 1) if y not in ('', None)]
|
||||
if len(l) > 1:
|
||||
# It MAY be a key:value.
|
||||
if re.search('^\s+', l[1]) and last_key != 'User Notice':
|
||||
# It's a value.
|
||||
last_key = l[0].strip()
|
||||
exts['certificatePolicies'][last_key] = l[1].strip()
|
||||
elif re.search('^\s+', l[1]):
|
||||
k = l[0].strip()
|
||||
exts['certificatePolicies'][last_key][k] = l[1].strip()
|
||||
else:
|
||||
# Standalone key line
|
||||
last_key = l[0].strip()
|
||||
exts['certificatePolicies'][last_key] = {}
|
||||
# ct_precert_scts
|
||||
# another mess.
|
||||
_tmp = copy.deepcopy(exts['ct_precert_scts'])
|
||||
exts['ct_precert_scts'] = {}
|
||||
last_key = None
|
||||
last_sub_key = None
|
||||
cnt = 0
|
||||
for i in [n.strip() for n in _tmp]:
|
||||
l = [y for y in i.split(':', 1) if y not in ('', None)]
|
||||
if len(l) > 1:
|
||||
print(l)
|
||||
# Is it a line continuation (of a hex value)?
|
||||
if ((re.search('^[0-9A-Z]{2}$', l[0])) and
|
||||
(re.search('^[0-9A-Z:]*:?$', ':'.join(l)))):
|
||||
exts['ct_precert_scts'][last_key][cnt][last_sub_key] += \
|
||||
':'.join(l)
|
||||
continue
|
||||
# It MAY be a key:value.
|
||||
if re.search('^\s+', l[1]) and (
|
||||
last_key !=
|
||||
'Signed Certificate Timestamp'):
|
||||
# It's a value.
|
||||
last_key = l[0].strip()
|
||||
val = l[1].strip()
|
||||
if val.lower() == 'none':
|
||||
val = None
|
||||
exts['ct_precert_scts'][last_key] = val
|
||||
elif re.search('^\s+', l[1]):
|
||||
last_sub_key = l[0].strip()
|
||||
val = l[1].strip()
|
||||
if val.lower() == 'none':
|
||||
val = None
|
||||
if last_sub_key == 'Signature':
|
||||
val += ' '
|
||||
exts['ct_precert_scts'][last_key][cnt][last_sub_key] = val
|
||||
else:
|
||||
# Standalone key line
|
||||
last_key = l[0].strip()
|
||||
if last_key == 'Signed Certificate Timestamp':
|
||||
if last_key not in exts['ct_precert_scts'].keys():
|
||||
exts['ct_precert_scts'][last_key] = [{}]
|
||||
else:
|
||||
exts['ct_precert_scts'][last_key].append({})
|
||||
cnt += 1
|
||||
# some laaaast bit of cleanup...
|
||||
if 'Signed Certificate Timestamp' in exts['ct_precert_scts'].keys():
|
||||
for i in exts['ct_precert_scts']['Signed Certificate Timestamp']:
|
||||
if 'Signature' in i.keys():
|
||||
d = i['Signature'].split()
|
||||
i['Signature'] = {d[0]: d[1]}
|
||||
return(exts)
|
||||
|
||||
def get_domain_from_url(self, url):
|
||||
orig_url = url
|
||||
# Needed in case a URL is passed with no http:// or https://, etc.
|
||||
url = re.sub('^((ht|f)tps?://)*',
|
||||
'https://',
|
||||
url,
|
||||
re.IGNORECASE).lower()
|
||||
if not self.validURL(url):
|
||||
raise ValueError(('{0} is not a valid URL').format(orig_url))
|
||||
domain = parse.urlparse(url).netloc
|
||||
return(domain)
|
||||
|
||||
def validIP(self, ip):
|
||||
is_valid = False
|
||||
try:
|
||||
ipaddress.ip_address(self.target)
|
||||
is_valid = True
|
||||
except ValueError:
|
||||
pass
|
||||
return(is_valid)
|
||||
|
||||
def validDomain(self, domain):
|
||||
is_valid = False
|
||||
try:
|
||||
validators.domain(domain)
|
||||
is_valid = True
|
||||
except ValidationFailure:
|
||||
pass
|
||||
return(is_valid)
|
||||
|
||||
def validURL(self, url):
|
||||
is_valid = False
|
||||
try:
|
||||
validators.url(url)
|
||||
is_valid = True
|
||||
except ValidationFailure:
|
||||
pass
|
||||
return(is_valid)
|
||||
|
||||
def validPath(self, path):
|
||||
is_valid = False
|
||||
if os.path.isfile(path):
|
||||
is_valid = True
|
||||
return(is_valid)
|
||||
|
||||
def get_type(self):
|
||||
if self.force_type:
|
||||
# Just run the validator and some cleanup.
|
||||
if self.force_type == 'url':
|
||||
self.target = self.get_domain_from_url(self.target)
|
||||
chk = self.validURL(self.target)
|
||||
if chk:
|
||||
self.force_type = 'domain'
|
||||
elif self.force_type == 'ip':
|
||||
chk = self.validIP(self.target)
|
||||
elif self.force_type == 'domain':
|
||||
chk = self.validDomain(self.target)
|
||||
elif self.force_type == 'file':
|
||||
self.target = os.path.abspath(os.path.expanduser(self.target))
|
||||
chk = self.validPath(self.target)
|
||||
if not chk:
|
||||
raise TypeError(('{0} does not appear to be a valid ' +
|
||||
'instance of type {1}'.format(self.target,
|
||||
self.force_type)
|
||||
))
|
||||
if self.force_type in ('url', 'domain', 'ip'):
|
||||
self.remote = True
|
||||
else:
|
||||
self.remote = False
|
||||
return()
|
||||
# Is it an IP address?
|
||||
if self.validIP(self.target):
|
||||
self.force_type = 'ip'
|
||||
return()
|
||||
# Is it a filepath?
|
||||
fpath = os.path.abspath(os.path.expanduser(self.target))
|
||||
if self.validPath(fpath):
|
||||
self.target = fpath
|
||||
self.force_type = 'file'
|
||||
return()
|
||||
# Is it a domain?
|
||||
if self.validDomain(self.target):
|
||||
self.force_type = 'domain'
|
||||
return()
|
||||
# Lastly, is it a URL?
|
||||
if self.validURL(self.target):
|
||||
domain = self.get_domain_from_url(self.target)
|
||||
if self.validDomain(domain):
|
||||
self.target = domain
|
||||
self.force_type = 'domain'
|
||||
if not self.force_type: # We couldn't detect it
|
||||
raise RuntimeError(('Automatic type detection of {0} requested ' +
|
||||
'but we could not determine what type of ' +
|
||||
'resource it is'))
|
||||
return()
|
||||
|
||||
def parseArgs():
|
||||
args = argparse.ArgumentParser()
|
||||
args.add_argument('-e', '--extensions',
|
||||
dest = 'extensions',
|
||||
action = 'store_true',
|
||||
help = ('If specified, include ALL extension info ' +
|
||||
'(this DRASTICALLY increases the output. You ' +
|
||||
'have been warned)'))
|
||||
args.add_argument('-a', '--alt-names',
|
||||
dest = 'alt_names',
|
||||
action = 'store_true',
|
||||
help = ('If specified, ONLY include the SAN (Subject ' +
|
||||
'Alt Name) extension. This is highly ' +
|
||||
'recommended over -e/--extensions. Ignored if ' +
|
||||
'-e/--extensions is set (as the SANs are ' +
|
||||
'included in that)'))
|
||||
args.add_argument('-j','--json',
|
||||
dest = 'json_fmt',
|
||||
action = 'store_true',
|
||||
help = ('If specified, return the results in JSON'))
|
||||
args.add_argument('-f', '--force',
|
||||
choices = ['url', 'domain', 'ip', 'file'],
|
||||
default = None,
|
||||
help = ('If specified, force the TARGET to be parsed ' +
|
||||
'as the given type'))
|
||||
args.add_argument('-p', '--port',
|
||||
dest = 'port',
|
||||
type = int,
|
||||
default = 443,
|
||||
help = ('Use a port other than 443 (only used for ' +
|
||||
'URL/domain/IP address targets)'))
|
||||
args.add_argument('-t', '--cert-type',
|
||||
dest = 'cert_type',
|
||||
default = 'pem',
|
||||
choices = ['pem', 'asn1'],
|
||||
help = ('The type of certificate (only used for '
|
||||
'file targets). Note that "DER"-encoded ' +
|
||||
'certificates should use "asn1". The default ' +
|
||||
'is pem'))
|
||||
# TODO: I think the starttls process depends on the protocol? If so, this...
|
||||
# won't be feasible.
|
||||
# args.add_argument('-s', '--starttls',
|
||||
# dest = 'starttls',
|
||||
# action = 'store_true',
|
||||
# help = ('If specified, initiate STARTTLS on the ' +
|
||||
# 'target instead of pure SSL/TLS'))
|
||||
args.add_argument('TARGET',
|
||||
help = ('The target to gather cert info for. Can be a ' +
|
||||
'filepath (to the certificate, not key etc.), ' +
|
||||
'a URL/domain, or IP address'))
|
||||
return(args)
|
||||
|
||||
def main():
|
||||
args = vars(parseArgs().parse_args())
|
||||
args['target'] = copy.deepcopy(args['TARGET'])
|
||||
del(args['TARGET'])
|
||||
p = CertParse(**args)
|
||||
p.getCert()
|
||||
p.parseCert()
|
||||
p.print()
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
Loading…
Reference in New Issue
Block a user