From 99920111ff6fe9b04eb2ba3f84a8c494420f23f5 Mon Sep 17 00:00:00 2001 From: Noah Laptop Date: Wed, 13 Mar 2019 20:33:37 -0700 Subject: [PATCH] added domain to description, fixed the "convert" command --- tools/pycert_bearssl/cert_util.py | 68 ++++++++++++++++---------- tools/pycert_bearssl/pycert_bearssl.py | 35 +++++++++---- 2 files changed, 67 insertions(+), 36 deletions(-) diff --git a/tools/pycert_bearssl/cert_util.py b/tools/pycert_bearssl/cert_util.py index 4fa4cd8..75b286e 100644 --- a/tools/pycert_bearssl/cert_util.py +++ b/tools/pycert_bearssl/cert_util.py @@ -108,6 +108,7 @@ CROOTCA_TEMPLATE = """\ # - cert_label: The certificate's name field (Usually CN, in the subject) # - cert_issue: The certificate's issuer string # - cert_subject: The certificate's subject string +# - cert_domain: The domains polled by this tool that returned this certificate CCERT_DESC_TEMPLATE = """\ * Index: {cert_num} * Label: {cert_label} @@ -171,18 +172,6 @@ def get_server_root_cert(address, port, certDict): return None return certDict[cn_hash] -def filter_duplicate_x509(certs): - serial_numbers = set() - out_certs = list() - # filter duplicate certs - for cert in certs: - # Skip duplicate certs where required. - if cert.get_serial_number() in serial_numbers: - continue - out_certs.append(cert) - serial_numbers.add(cert.get_serial_number()) - return out_certs - def bytes_to_c_data(mah_bytes, length=None): """Converts a byte array to a CSV C array data format, with endlines! e.g: 0x12, 0xA4, etc. @@ -199,7 +188,13 @@ def bytes_to_c_data(mah_bytes, length=None): # join, wrap, and return return textwrap.fill(''.join(ret), width=6*12 + 5, initial_indent=' ', subsequent_indent=' ', break_long_words=False) -def decribe_cert_object(cert, cert_num): +def decribe_cert_object(cert, cert_num, domain=None): + """ + Formats a string describing a certificate object, including the domain + being used and the index in the trust anchor array. Cert should be a + x509 object, domain should be a string name, and cert_num should be + an integer. + """ # get the label from the subject feild on the certificate label = "" com = dict(cert.get_subject().get_components()) @@ -211,16 +206,20 @@ def decribe_cert_object(cert, cert_num): label = com[b'O'].decode("utf-8") # return the formated string crypto = cert.to_cryptography() - return CCERT_DESC_TEMPLATE.format( + out_str = CCERT_DESC_TEMPLATE.format( cert_num=cert_num, cert_label=label, cert_subject=crypto.subject.rfc4514_string(), ) - + # if domain, then add domain entry + if domain is not None: + out_str += "\n * Domain(s): " + domain + return out_str -def x509_to_header(x509Certs, cert_var, cert_length_var, output_file, keep_dupes): +def x509_to_header(x509Certs, cert_var, cert_length_var, output_file, keep_dupes, domains=None): """Combine a collection of PEM format certificates into a single C header with the combined cert data in BearSSL format. x509Certs should be a list of pyOpenSSL x590 objects, + domains should be a list of respective domain strings (in same order as x509Certs), cert_var controls the name of the cert data variable in the output header, cert_length_var controls the name of the cert data length variable/define, output is the output file (which must be open for writing). Keep_dupes is a boolean to indicate if duplicate @@ -228,24 +227,41 @@ def x509_to_header(x509Certs, cert_var, cert_length_var, output_file, keep_dupes """ cert_description = '' certs = x509Certs - if not keep_dupes: - certs = filter_duplicate_x509(x509Certs) # Save cert data as a C style header. # start by building each component cert_data = "" + # hold an array of static array strings (TA_RSA_N) static_arrays = list() + # same with CA entries CAs = list() + # descriptions cert_desc = list() + # track the serial numbers so we can find duplicates + cert_ser = list() for i, cert in enumerate(certs): + # calculate the index shifted from duplicates (if any) + cert_index = len(CAs) + # deduplicate certificates + if not keep_dupes and cert.get_serial_number() in cert_ser: + # append the domain we used it for into the cert description + if domains is not None: + cert_desc[cert_ser.index(cert.get_serial_number())] += ", " + domains[i] + # we don't need to generate stuff for this certificate + continue + # record the serial number for later + cert_ser.append(cert.get_serial_number()) # add a description of the certificate to the array - cert_desc.append(decribe_cert_object(cert, i)) + if domains is None: + cert_desc.append(decribe_cert_object(cert, cert_index)) + else: + cert_desc.append(decribe_cert_object(cert, cert_index, domain=domains[i])) # build static arrays containing all the keys of the certificate # start with distinguished name # get the distinguished name in bytes dn_bytes_str = bytes_to_c_data(cert.get_subject().der()) static_arrays.append(CRAY_TEMPLATE.format( ray_type="unsigned char", - ray_name=DN_PRE + str(i), + ray_name=DN_PRE + str(cert_index), ray_data=dn_bytes_str)) # next, the RSA public numbers pubkey = cert.get_pubkey() @@ -254,19 +270,19 @@ def x509_to_header(x509Certs, cert_var, cert_length_var, output_file, keep_dupes n_bytes_str = bytes_to_c_data(numbers.n.to_bytes(pubkey.bits() // 8, byteorder="big")) static_arrays.append(CRAY_TEMPLATE.format( ray_type="unsigned char", - ray_name=RSA_N_PRE + str(i), + ray_name=RSA_N_PRE + str(cert_index), ray_data=n_bytes_str)) # and then the exponent e_bytes_str = bytes_to_c_data(numbers.e.to_bytes(math.ceil(numbers.e.bit_length() / 8), byteorder="big")) static_arrays.append(CRAY_TEMPLATE.format( ray_type="unsigned char", - ray_name=RSA_E_PRE + str(i), + ray_name=RSA_E_PRE + str(cert_index), ray_data=e_bytes_str)) # format the root certificate entry CAs.append(CROOTCA_TEMPLATE.format( - ta_dn_name=DN_PRE + str(i), - rsa_number_name=RSA_N_PRE + str(i), - rsa_exp_name=RSA_E_PRE + str(i))) + ta_dn_name=DN_PRE + str(cert_index), + rsa_number_name=RSA_N_PRE + str(cert_index), + rsa_exp_name=RSA_E_PRE + str(cert_index))) # concatonate it all into the big header file template # cert descriptions cert_desc_out = '\n * \n'.join(cert_desc) @@ -281,6 +297,6 @@ def x509_to_header(x509Certs, cert_var, cert_length_var, output_file, keep_dupes guard_name=os.path.splitext(output_file.name)[0].upper(), cert_description=cert_desc_out, cert_length_var=cert_length_var, - cert_length=str(len(certs)), + cert_length=str(len(CAs)), cert_data=cert_data_out, )) \ No newline at end of file diff --git a/tools/pycert_bearssl/pycert_bearssl.py b/tools/pycert_bearssl/pycert_bearssl.py index a92beaa..c76df3a 100644 --- a/tools/pycert_bearssl/pycert_bearssl.py +++ b/tools/pycert_bearssl/pycert_bearssl.py @@ -18,6 +18,7 @@ import cert_util import click import certifi +from OpenSSL import crypto # Default name for the cert length varible CERT_LENGTH_NAME = "TAs_NUM" @@ -82,7 +83,7 @@ def download(port, cert_var, cert_length_var, output, use_store, keep_dupes, dom # append cert to array down_certs.append(cert) # Combine PEMs and write output header. - cert_util.x509_to_header(down_certs, cert_var, cert_length_var, output, keep_dupes) + cert_util.x509_to_header(down_certs, cert_var, cert_length_var, output, keep_dupes, domains=domain) @pycert_bearssl.command(short_help='Convert PEM certs into a C header.') @@ -92,33 +93,47 @@ def download(port, cert_var, cert_length_var, output, use_store, keep_dupes, dom help='name of the define in the header which will contain the length of the certificate data (default: {0})'.format(CERT_LENGTH_NAME)) @click.option('--output', '-o', type=click.File('w'), default='certificates.h', help='name of the output file (default: certificates.h)') -@click.option('--full-chain', '-f', is_flag=True, default=False, - help='use the full certificate chain and not just the root/last cert (default: false, root cert only)') +@click.option('--use-store', '-s', type=click.File('r'), default=certifi.where(), + help='the location of the .pem file containing a list of trusted root certificates (default: use certifi.where())') @click.option('--keep-dupes', '-d', is_flag=True, default=False, help='write all certs including any duplicates (default: remove duplicates)') @click.argument('cert', type=click.File('r'), nargs=-1) -def convert(cert_var, cert_length_var, output, full_chain, keep_dupes, cert): +def convert(cert_var, cert_length_var, output, use_store, keep_dupes, cert): """Convert PEM certificates into a C header that can be imported into a sketch. Specify each certificate to encode as a separate argument (each must be in PEM format) and they will be merged into a single file. By default the file 'certificates.h' will be created, however you can change the name of the file with the --output option. If a chain of certificates is found then only the root certificate (i.e. - the last in the chain) will be saved. However you can override this and - force the full chain to be saved with the --full-chain option. + the last in the chain) will be saved. Example of converting a foo.pem certificate into a certificates.h header: pycert convert foo.pem Example of converting foo.pem and bar.pem certificates into data.h: pycert convert foo.pem bar.pem """ + # prepare root certificate store + cert_obj_store = cert_util.parse_root_certificate_store(use_store) + cert_dict = dict([(cert.get_subject().hash(), cert) for cert in cert_obj_store]) # Load all the provided PEM files. - pems = [] + cert_objs = [] for c in cert: cert_pem = c.read() - click.echo('Loaded certificate {0}'.format(c.name)) - pems.append(cert_pem) + cert_parsed = crypto.load_certificate(crypto.FILETYPE_PEM, cert_pem) + if cert_parsed is None: + click.echo('Failed to load certificate {0}'.format(c.name)) + else: + click.echo('Loaded certificate {0}'.format(c.name)) + cert_objs.append(cert_parsed) + # find a root certificate for each + root_certs = [] + for i, c in enumerate(cert_objs): + cn_hash = c.get_issuer().hash() + if cn_hash not in cert_dict: + click.echo('Could not find a root certificate for {0}'.format(cert[i].name)) + else: + root_certs.append(cert_dict[cn_hash]) # Combine PEMs and write output header. - PEM_to_header(pems, cert_var, cert_length_var, output, full_chain, keep_dupes) + cert_util.x509_to_header(root_certs, cert_var, cert_length_var, output, keep_dupes) if __name__ == '__main__':