#!/usr/bin/python3 # A script to read smartcard certificates # as a useful crosscheck/debugging tool # # David Friedlander, ADNET Systems, 25 March 2020 # written at home during coronavirus outbreak # Updated for python3 (02/22/2022, dpf) with Apple removing python 2.7 in macOS12.3 import os, re import pprint # pretty printer #akin to perl Dumper from datetime import datetime import time from time import mktime pp = pprint.PrettyPrinter(indent=4) verbose = False print ("Running: 'system_profiler SPSmartCardsDataType' and parsing output...") sysprof_output_raw = os.popen('system_profiler SPSmartCardsDataType').read() sysprof_output_raw = sysprof_output_raw.rstrip() # there are actually two newlines sysprof_output = re.split('\n', sysprof_output_raw) #begin_line = '-----BEGIN CERTIFICATE-----' #end_line = '-----END CERTIFICATE-----' found_certs = {} found_certs['keychain'] = {} found_certs['token'] = {} sc_type = "" def create_cert_filename(sc_type, cert_num): #This function simply formats a filename with the smartcard type, #the certificate number, and an appropriately padded number for those <10. str_cert_num = str(cert_num) if len(str_cert_num) < 2: str_cert_num = "0" + str_cert_num #prepend on 0-9 (=> 00-09). certificate_filename = '/tmp/certificate.' + sc_type + "." + str_cert_num if verbose: print ("Certificate filename is " + certificate_filename) return certificate_filename def create_cert_file(sc_type, cert_num): # We have the certificate contents in a dict, now lets # write it out to a file. ('openssl x509' can also take STDIN input) certificate_filename = create_cert_filename(sc_type, cert_num) if os.path.exists(certificate_filename): os.unlink(certificate_filename) f = open (certificate_filename, 'w') f.write(found_certs[sc_type][cert_num]['begin_line'] + '\n') f.write(found_certs[sc_type][cert_num]['cert_contents'] + '\n') f.write(found_certs[sc_type][cert_num]['end_line'] + '\n') #f.write('\n') f.close() if verbose: print ("Created " + certificate_filename) def decode_cert(sc_type, cert_num): # Run the "openssl x509" command and parse the output. #print ("\tNow trying to decode %s : %s " % (sc_type, cert_num)) certificate_filename = create_cert_filename(sc_type, cert_num) x509_raw = os.popen('openssl x509 -noout -text -in ' + certificate_filename).read() x509_raw = x509_raw.rstrip() x509_output = re.split('\n', x509_raw) # https://stackoverflow.com/questions/16899247/how-can-i-decode-a-ssl-certificate-using-python (but I want to use native tools, for portability) # import ssl #(now at top) # cert_dict = ssl._ssl._test_decode_cert(certificate_filename) # pprint.pprint(cert_dict) # print (x509_read) #works # There are more elegant 'pythonic' ways to read X509 certificates # but they all rely on non-standard libraries, but we value portability # over all, so users can debug. num = 0 for num, line in enumerate(x509_output): #print ("L63: %s %s" % (num, line)) #works if re.search(r'Subject: C=US', line): # line of the form # " Subject: C=US, O=U.S. Government, OU=NASA, OU=People/UID=dfriedla, CN=David Friedlander (affiliate)" # If we merely split on "comma space" then names get broken apart. subj_fields = re.split(',? [A-Z]{1,2}=', line) ou = subj_fields[3] ou_2 = subj_fields[4] #person or serialNumber try: # If there is a serialnumber, there is no 5th field for cn cn = subj_fields[5] cn = re.sub('\s+\(affiliate\)', '', cn) except: cn = '' auid = re.sub('People/UID=', '', ou_2) print ("\t Subject [%s] %s [%s] %s" % \ (certificate_filename, ou, auid, cn)) if re.search(r'Key Usage:', line): x509_purpose = x509_output[num+1] x509_purpose = re.sub(r'^\s+', '', x509_purpose) print ("\tX509 Purpose [%s] is [%s]" % (certificate_filename,x509_purpose)) found_certs[sc_type][cert_num]['x509_purpose'].append(x509_purpose)#not used def is_cert_still_valid(sc_type,cert_num): # Simple question: how does the certificate expiration compare # with right now? # (We are ignoring the timezone offset-- let's hope users don't # get that close to their expiration dates that it matters!) # This date is read from the "valid" line in the system_profiler # output, _not_ read from the x09 internals of the certificate. # (Should match, of course) valid_line = found_certs[sc_type][cert_num]['valid_line'] cert_end_date = re.search(r'Valid from:.* to: (.*), SSL trust', valid_line).group(1) # For now, we will strip off the relative time zone (time.strptime # cannot seem to handle it?) cert_end_date = re.sub(r' \+0000', '', cert_end_date) end_date_tm =time.strptime(cert_end_date, "%Y-%m-%d %H:%M:%S") # end_date_tm is a 'time.struct_time' object. # Let's convert to a datetime one. end_date = datetime.fromtimestamp(mktime(end_date_tm)) now = datetime.now() certificate_filename = create_cert_filename(sc_type, cert_num) if end_date > now: print ("Certificate [%s] expires in the future: %s" \ % (certificate_filename, cert_end_date)) return True else: print ("Expired certificate [%s] %s" \ % (certificate_filename, cert_end_date)) return False ####### main program ################# for num, line in enumerate(sysprof_output): if re.search('Available SmartCards', line): # Either 'keychain' or 'token' sc_type = re.search(r'Available SmartCards \((.*)\):', line).group(1) if re.search('Kind:', line): kind_line=line cert_num = re.search('^\s+#(\d+): Kind', kind_line).group(1) cert_num = int(cert_num) #cast valid_line = sysprof_output[num+1] begin_line = sysprof_output[num+3] cert_contents = sysprof_output[num+4] end_line = sysprof_output[num+5] if verbose: print ("^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^") print ("L36: TYPE: %s cert number %s" % (sc_type, cert_num)) #cert_num = str(cert_num) # commented out, b/c apparently, integers are OK cert_info = { 'kind_line': kind_line, 'valid_line': valid_line, 'begin_line': begin_line, 'cert_contents': cert_contents, 'end_line': end_line, 'x509_purpose': [] #for use in decode_cert() } found_certs[sc_type][cert_num] = cert_info #There are two type of certificates in the output: token and keychain, #but they are identical for any given certificate number! (what's the point) target_type = 'token' found_number = len(found_certs[target_type]) if found_number > 1: print ("There were %s certificates found marked '%s'" % \ (found_number, target_type)) for cert_number in found_certs[target_type]: #print ("L152: " + found_certs[target_type][cert_number]['valid_line']) create_cert_file(target_type, cert_number) cert_valid = is_cert_still_valid(sc_type,cert_number) if cert_valid: decode_cert(sc_type,cert_number) print ("\nYou can decode any of these files in /tmp with") print ("openssl x509 -noout -text -in ") else: print ("There were NO certificates found. Is your badge inserted?") print ("Here's all we saw in output (should be more than 100 lines) :") for line in sysprof_output: print (line) # cryptography package, as described here: # https://stackoverflow.com/questions/16899247/how-can-i-decode-a-ssl-certificate-using-python #yes but *really* non-portable to give to others. Will stick with # openssl x509 -noout -text -in #pp.pprint(found_certs)