PX4-Autopilot/Tools/signtool.py
2022-12-19 13:12:42 +01:00

350 lines
11 KiB
Python

#!/usr/bin/env python3
from os import EX_CANTCREAT
import nacl.encoding
import nacl.signing
import nacl.hash
import struct
import zlib
import json
import base64
from pathlib import Path
from dataclasses import dataclass
import cryptotools
import argparse
import os
# Dictionary describing the possible TOC flags, they can be OR'ed together
toc_flag_dict = {
'TOC_FLAG1_BOOT': 0x1,
'TOC_FLAG1_VTORS': 0x2,
'TOC_FLAG1_CHECK_SIGNATURE': 0x4,
'TOC_FLAG1_DECRYPT': 0x8,
'TOC_FLAG1_COPY': 0x10,
'TOC_FLAG1_RDCT': 0x80,
}
# Dataclasses describing the TOC data, used to parse the to and from binary
@dataclass
class TOC_start:
start_magic: str = None
version: int = None
STRUCT_STRUCTURE = "<4sI" # type defininig string from the STRUCT package
# Dataclass to describe the TOC for 2 entries, one describing the signature typen
# and the second one describing the signature location.
@dataclass
class TOC_entry:
toc_position: int = 0
app_name: str = None
app_start: int = None
app_end: int = None
app_target: int = None
app_signature_idx: int = None
app_signature_key: int = None
app_encryption_key: int = None
app_flags1: int = None
app_reserved: int = None
sig_name: str = None
sig_start: int = None
sig_end: int = None
sig_target: int = None
sig_signature_idx: int = None
sig_signature_key: int = None
sig_encryption_key: int = None
sig_flags1: int = None
sig_reserved: int = None
# type defininig string from the STRUCT package
STRUCT_STRUCTURE = "<4sIIIBBBBI4sIIIBBBBI"
def toc2bin(data):
'''
Takes as TOC data class and converts it to the binary representation.
Prepare a TOC_entry data class with values and hand it over to this function.
data: is a dataclass TOC_entry
retrun: a packed binary to add to the px4 bin file
'''
return struct.pack(data.STRUCT_STRUCTURE,
data.app_name, data.app_start,
data.app_end, data.app_target,
data.app_signature_idx,
data.app_signature_key,
data.app_encryption_key,
data.app_flags1,
data.app_reserved,
data.sig_name, data.sig_start,
data.sig_end, data.sig_target,
data.sig_signature_idx,
data.sig_signature_key,
data.sig_encryption_key,
data.sig_flags1,
data.sig_reserved)
def bin2toc_start(bin):
'''
Takes binary data and unpacks a Toc_start header.
bin: binary data to parse for the TOC header.
return: a dataclass TOC_start
'''
data = TOC_start()
(data.start_magic, data.version) = struct.unpack(data.STRUCT_STRUCTURE, bin)
return data
def bin2toc_entry(bin):
'''
Takes binary data and unpacks it into a TOC entry dataclass.
bin: Binary data to unpack the TOC entry from.
return: A dataclass of TOC_entry
'''
data = TOC_entry()
(data.app_name, data.app_start,
data.app_end, data.app_target,
data.app_signature_idx,
data.app_signature_key,
data.app_encryption_key,
data.app_flags1,
data.app_reserved,
data.sig_name, data.sig_start,
data.sig_end, data.sig_target,
data.sig_signature_idx,
data.sig_signature_key,
data.sig_encryption_key,
data.sig_flags1,
data.sig_reserved
) = struct.unpack(data.STRUCT_STRUCTURE, bin)
return data
def parse_toc(bin):
'''
Searches for the TOC in the binary data.
This function looks for a TOC within certain boundaries in the TOC. It
also checks to find a valid TOC_end.
Throws exceptions, if a toc is not found, wrong version or end is not found.
bin: Binary data to look for the TOC
return: dataclass with the TOC entry
'''
# This is a fixed address from the linker file, TOC is placed after.
BOOT_DELAY_ADDR = 0x200
TOC_LEN_MAX = 64
EXPECTED_TOC_VERSION = 1
toc_start_len = struct.calcsize(TOC_start().STRUCT_STRUCTURE)
toc_entry_len = struct.calcsize(TOC_entry().STRUCT_STRUCTURE)
start_indx = bin.find(b'TOC', BOOT_DELAY_ADDR,
BOOT_DELAY_ADDR + TOC_LEN_MAX)
if start_indx <= 0:
raise Exception('TOC not found')
toc_start_header = bin2toc_start(
bin[start_indx:(start_indx+toc_start_len)])
print('TOC start found', toc_start_header,'@: ',hex(start_indx))
if toc_start_header.version != EXPECTED_TOC_VERSION:
raise Exception('Wrong TOC version!')
t = bin2toc_entry(
bin[start_indx+toc_start_len: (start_indx+toc_start_len+toc_entry_len)])
t.toc_position = start_indx+toc_start_len
print(t)
indx = bin.find(b'END', start_indx, start_indx + 512)
if indx <= 0:
toc_end = False
raise Exception('TOC end not found')
return t
def unpackPx4(file_path):
'''
Unpacks a .px4 file to get access to its binary data.
filepath: Path to a px4 file to extract.
return: A tuple of (binary data, json_data) of the file.
'''
# read the file
with open(file_path, "r") as f:
desc = json.load(f)
image = bytearray(zlib.decompress(base64.b64decode(desc['image'])))
return image, desc
def packPx4(bin_image, json_data, file_path, pub_key):
'''
Packs a new .px4 file with given binary data and information from jason file.
bin_image: New binary data with signature to add to .px4 file
json_data: The json data from the previously parsed .px4.
file_path: File path to then new .px4.sec image.
return: Nothing
'''
head,tail=os.path.splitext(file_path)
with open(head +'_signed.px4', 'w') as f:
json_data['signed'] = 'Hash512_Ed25519'
json_data['pub_key'] = pub_key.decode('utf-8')
json_data['image_size'] = len(bin_image)
json_data['image'] = base64.b64encode(
zlib.compress(bin_image, 9)).decode('utf-8')
print('Pack new signed.px4 file with signature')
json.dump(json_data, f, indent=4)
def ed25519_sign(private_key, signee_bin):
"""
This function creates the signature. It takes the private key and the binary file
and returns the tuple (signature, public key)
"""
signing_key = nacl.signing.SigningKey(
private_key, encoder=nacl.encoding.HexEncoder)
# Sign a message with the signing key
signed = signing_key.sign(signee_bin, encoder=nacl.encoding.RawEncoder)
# Obtain the verify key for a given signing key
verify_key = signing_key.verify_key
# Serialize the verify key to send it to a third party
verify_key_hex = verify_key.encode(encoder=nacl.encoding.HexEncoder)
return signed.signature, verify_key_hex
def write_toc(toc_old, bin, signature_name, new_pub_key_index, new_flags):
'''
Writes a new TOC entry.
toc_old: The parsed data_class with the data of the old TOC
bin: The binary to insert the new TOC
signature_name: 4 char name of the newly added signature
new_pub_key_index: Key index of the public key belongs to the signature
new_flags: New Toc flags from toc_flag_dict dictionary.
return: New bin with modified TOC
'''
toc_new = toc_old
toc_new.app_signature_key = new_pub_key_index
toc_new.app_flags1 = new_flags
if len(signature_name) != 4:
raise Exception('Signature name has not the right length')
toc_new.app_name = bytes(signature_name, 'utf_8')
toc_new_bin = toc2bin(toc_new)
toc_entry_len = struct.calcsize(TOC_entry().STRUCT_STRUCTURE)
bin[toc_old.toc_position:toc_old.toc_position+toc_entry_len] = toc_new_bin
return bin
def cli():
'''
Comand lined interface to the signtool.
See usage comand.
return: class with parsed arguments.
'''
parser = argparse.ArgumentParser(
description='Tool to extract and find crypto TOC from .px4 file. And append a signature from a given private key')
# defining arguments for parser object
parser.add_argument("--signee", type=str,
metavar="file_path", default=None,
help="Opens and reads the specified px4 file.")
parser.add_argument("--private_key", type=str,
metavar="key string", default=None,
help="Private key to sign the px4 image")
parser.add_argument("--key_index", type=int,
metavar="Number", default=None,
help="Index of the public key used to verify the binary")
parser.add_argument("--TOC_flags", type=str, choices=['TOC_FLAG1_BOOT', 'TOC_FLAG1_RDCT'],
default=None,
help="TOC flags to indicate signature")
args = parser.parse_args()
return args
def sign(file_path, private_key, key_index, TOC_flags):
'''
Signs a binary file and updates TOC accordingly.
Reads a .px4 or a .bin at specified location and writes a new _signed.[px4|bin] file at the same location.
file_path: Path to a .px4 or .bin file to sign.
private_key: String of the private key used to sign the binary.
key_index: Index of the public key used to verify signature.
TOC_flags: New toc flags to be written.
'''
head,tail=os.path.splitext(file_path)
if(tail == '.px4'):
bin, json_data = unpackPx4(file_path)
elif(tail == '.bin'):
with open(file_path,mode='rb') as f:
bin =bytearray(f.read())
else:
raise Exception('Error: Unknown file type')
toc_old = parse_toc(bin)
new_bin = write_toc(toc_old, bin, 'MSTR', key_index,
toc_flag_dict[TOC_flags])
app_len = toc_old.app_end - toc_old.app_start
print('Calculate new signature')
signature, pub_key = cryptotools.ed25519_sign(
private_key, bytes(new_bin[0:app_len]))
# Append signature to binary
print('Append signature to binary: ',signature.hex(),'\n','length: ',app_len )
sig_start=toc_old.sig_start - toc_old.app_start
sig_end = toc_old.sig_end - toc_old.app_start
if sig_start != app_len:
padding = bytearray(b'\xff')*(sig_start - app_len)
new_bin += padding
new_bin[toc_old.sig_start-toc_old.app_start:toc_old.sig_end -
toc_old.app_start] = signature
if(tail == '.px4'):
packPx4(new_bin, json_data, file_path,pub_key)
elif(tail == '.bin'):
head,tail=os.path.splitext(file_path)
with open(head +'_signed.bin',mode='wb') as f:
f.write(bin)
else:
raise Exception('Error: Unknown file type!')
if(__name__ == "__main__"):
args = cli()
# test input to the sign function if not run from CLI
# sign('PX4-Autopilot/build/px4_fmu-v5x_default/px4_fmu-v5x_default.px4',
# "8e969454c3f1ba924b4d436bc58b87d20c082ea99fb6a77b9e00f7b20dab0fd8",
# 0, 'TOC_FLAG1_BOOT')
# CLI test input:
# --signee /PX4-Autopilot/build/px4_fmu-v5x_default/px4_fmu-v5x_default.px4
# --private_key 8e969454c3f1ba924b4d436bc58b87d20c082ea99fb6a77b9e00f7b20dab0fd8 --key_index 0 --TOC_flags TOC_FLAG1_BOOT
sign(args.signee, args.private_key, args.key_index, args.TOC_flags)