added domain to description, fixed the "convert" command

This commit is contained in:
Noah Laptop 2019-03-13 20:33:37 -07:00
parent ac951f1b30
commit 99920111ff
2 changed files with 67 additions and 36 deletions

View file

@ -108,6 +108,7 @@ CROOTCA_TEMPLATE = """\
# - cert_label: The certificate's name field (Usually CN, in the subject) # - cert_label: The certificate's name field (Usually CN, in the subject)
# - cert_issue: The certificate's issuer string # - cert_issue: The certificate's issuer string
# - cert_subject: The certificate's subject string # - cert_subject: The certificate's subject string
# - cert_domain: The domains polled by this tool that returned this certificate
CCERT_DESC_TEMPLATE = """\ CCERT_DESC_TEMPLATE = """\
* Index: {cert_num} * Index: {cert_num}
* Label: {cert_label} * Label: {cert_label}
@ -171,18 +172,6 @@ def get_server_root_cert(address, port, certDict):
return None return None
return certDict[cn_hash] 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): def bytes_to_c_data(mah_bytes, length=None):
"""Converts a byte array to a CSV C array data format, with endlines! """Converts a byte array to a CSV C array data format, with endlines!
e.g: 0x12, 0xA4, etc. e.g: 0x12, 0xA4, etc.
@ -199,7 +188,13 @@ def bytes_to_c_data(mah_bytes, length=None):
# join, wrap, and return # join, wrap, and return
return textwrap.fill(''.join(ret), width=6*12 + 5, initial_indent=' ', subsequent_indent=' ', break_long_words=False) 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 # get the label from the subject feild on the certificate
label = "" label = ""
com = dict(cert.get_subject().get_components()) 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") label = com[b'O'].decode("utf-8")
# return the formated string # return the formated string
crypto = cert.to_cryptography() crypto = cert.to_cryptography()
return CCERT_DESC_TEMPLATE.format( out_str = CCERT_DESC_TEMPLATE.format(
cert_num=cert_num, cert_num=cert_num,
cert_label=label, cert_label=label,
cert_subject=crypto.subject.rfc4514_string(), 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 """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, 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 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 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 (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 = '' cert_description = ''
certs = x509Certs certs = x509Certs
if not keep_dupes:
certs = filter_duplicate_x509(x509Certs)
# Save cert data as a C style header. # Save cert data as a C style header.
# start by building each component # start by building each component
cert_data = "" cert_data = ""
# hold an array of static array strings (TA_RSA_N)
static_arrays = list() static_arrays = list()
# same with CA entries
CAs = list() CAs = list()
# descriptions
cert_desc = list() cert_desc = list()
# track the serial numbers so we can find duplicates
cert_ser = list()
for i, cert in enumerate(certs): 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 # 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 # build static arrays containing all the keys of the certificate
# start with distinguished name # start with distinguished name
# get the distinguished name in bytes # get the distinguished name in bytes
dn_bytes_str = bytes_to_c_data(cert.get_subject().der()) dn_bytes_str = bytes_to_c_data(cert.get_subject().der())
static_arrays.append(CRAY_TEMPLATE.format( static_arrays.append(CRAY_TEMPLATE.format(
ray_type="unsigned char", ray_type="unsigned char",
ray_name=DN_PRE + str(i), ray_name=DN_PRE + str(cert_index),
ray_data=dn_bytes_str)) ray_data=dn_bytes_str))
# next, the RSA public numbers # next, the RSA public numbers
pubkey = cert.get_pubkey() 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")) n_bytes_str = bytes_to_c_data(numbers.n.to_bytes(pubkey.bits() // 8, byteorder="big"))
static_arrays.append(CRAY_TEMPLATE.format( static_arrays.append(CRAY_TEMPLATE.format(
ray_type="unsigned char", ray_type="unsigned char",
ray_name=RSA_N_PRE + str(i), ray_name=RSA_N_PRE + str(cert_index),
ray_data=n_bytes_str)) ray_data=n_bytes_str))
# and then the exponent # and then the exponent
e_bytes_str = bytes_to_c_data(numbers.e.to_bytes(math.ceil(numbers.e.bit_length() / 8), byteorder="big")) 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( static_arrays.append(CRAY_TEMPLATE.format(
ray_type="unsigned char", ray_type="unsigned char",
ray_name=RSA_E_PRE + str(i), ray_name=RSA_E_PRE + str(cert_index),
ray_data=e_bytes_str)) ray_data=e_bytes_str))
# format the root certificate entry # format the root certificate entry
CAs.append(CROOTCA_TEMPLATE.format( CAs.append(CROOTCA_TEMPLATE.format(
ta_dn_name=DN_PRE + str(i), ta_dn_name=DN_PRE + str(cert_index),
rsa_number_name=RSA_N_PRE + str(i), rsa_number_name=RSA_N_PRE + str(cert_index),
rsa_exp_name=RSA_E_PRE + str(i))) rsa_exp_name=RSA_E_PRE + str(cert_index)))
# concatonate it all into the big header file template # concatonate it all into the big header file template
# cert descriptions # cert descriptions
cert_desc_out = '\n * \n'.join(cert_desc) 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(), guard_name=os.path.splitext(output_file.name)[0].upper(),
cert_description=cert_desc_out, cert_description=cert_desc_out,
cert_length_var=cert_length_var, cert_length_var=cert_length_var,
cert_length=str(len(certs)), cert_length=str(len(CAs)),
cert_data=cert_data_out, cert_data=cert_data_out,
)) ))

View file

@ -18,6 +18,7 @@
import cert_util import cert_util
import click import click
import certifi import certifi
from OpenSSL import crypto
# Default name for the cert length varible # Default name for the cert length varible
CERT_LENGTH_NAME = "TAs_NUM" 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 # append cert to array
down_certs.append(cert) down_certs.append(cert)
# Combine PEMs and write output header. # 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.') @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)) 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', @click.option('--output', '-o', type=click.File('w'), default='certificates.h',
help='name of the output file (default: certificates.h)') help='name of the output file (default: certificates.h)')
@click.option('--full-chain', '-f', is_flag=True, default=False, @click.option('--use-store', '-s', type=click.File('r'), default=certifi.where(),
help='use the full certificate chain and not just the root/last cert (default: false, root cert only)') 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, @click.option('--keep-dupes', '-d', is_flag=True, default=False,
help='write all certs including any duplicates (default: remove duplicates)') help='write all certs including any duplicates (default: remove duplicates)')
@click.argument('cert', type=click.File('r'), nargs=-1) @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 """Convert PEM certificates into a C header that can be imported into a
sketch. Specify each certificate to encode as a separate argument (each 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. 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 By default the file 'certificates.h' will be created, however you can change
the name of the file with the --output option. the name of the file with the --output option.
If a chain of certificates is found then only the root certificate (i.e. 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 the last in the chain) will be saved.
force the full chain to be saved with the --full-chain option.
Example of converting a foo.pem certificate into a certificates.h header: Example of converting a foo.pem certificate into a certificates.h header:
pycert convert foo.pem pycert convert foo.pem
Example of converting foo.pem and bar.pem certificates into data.h: Example of converting foo.pem and bar.pem certificates into data.h:
pycert convert foo.pem bar.pem 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. # Load all the provided PEM files.
pems = [] cert_objs = []
for c in cert: for c in cert:
cert_pem = c.read() cert_pem = c.read()
click.echo('Loaded certificate {0}'.format(c.name)) cert_parsed = crypto.load_certificate(crypto.FILETYPE_PEM, cert_pem)
pems.append(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. # 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__': if __name__ == '__main__':