diff --git a/README.md b/README.md index 494e8b4..f93ebcd 100644 --- a/README.md +++ b/README.md @@ -128,6 +128,33 @@ Yields FD_2.DRIVER.bin FD_3.ASW.bin FD_4.CAL.bin ``` +# Digitally signing BIN files +The following is how you sign a full bin. Optionally including some notes about the file and a secondary_key to sign it with (if you have your own private key). If you don't provide a secondary key, only the private key in `data/VW_Flash.key` is used +``` +python VW_Flash.py --action prepare --input_bin FILENAME --output_bin FILENAME_OUT [--signed [--notes "Some notes about the file" ] [--secondary_key PRIVATE_KEY ] ] +``` + +validation is simple as well. The following command would read in the input_bin and check the signatures against the public key in data/VW_Flash.pub as well as the secondary_key (if the file contains a dual signature) +``` +python VW_Flash.py --action validate --input_bin FILENAME [ --secondary_key PUBLIC_KEY ] +``` + +If you *do* want to sign files yourself, you can create a keypair with the following python code: +```python +from Crypto.PublicKey import RSA +key = RSA.generate(1024) + +private = open('private.key', 'wb') +public = open('public.key', 'wb') + +private.write(key.exportKey("PEM")) +public.write(key.public_key().exportKey("PEM")) + +private.close() +public.close() +exit() +``` + # Tools [VW_Flash.py](VW_Flash.py) provides a complete "port flashing" toolchain - it's a command line interface which has the capability of performing various operations, including fixing checksums for Application Software and Calibration blocks, fixing ECM2->ECM3 monitoring checksums for CAL, encrypting, compressing, and finally, flashing blocks to the ECU. diff --git a/VW_Flash.py b/VW_Flash.py index e0b0303..4dabd5c 100644 --- a/VW_Flash.py +++ b/VW_Flash.py @@ -18,6 +18,7 @@ import lib.modules.simos184 as simos184 import lib.modules.dq250mqb as dq250mqb import lib.modules.simosshared as simosshared +import lib.signature_tools as signature_tools import shutil @@ -67,6 +68,7 @@ "flash_unlock", "get_ecu_info", "get_dtcs", + "validate", ], required=True, ) @@ -124,6 +126,27 @@ required=False, ) +parser.add_argument( + "--signed", + help="If writing to an output_bin, sign it using the VW_Flash private key (and optionally, a secondary key)", + action="store_true", + required=False, +) + +parser.add_argument( + "--secondary_key", + help="If signing an output file, the optional secondary key used to sign it", + type=str, + required=False, +) + +parser.add_argument( + "--notes", + help="Used with signed bins, this is a string that will be included in the signature of the file", + type=str, + required=False, +) + parser.add_argument( "--interface", help="specify an interface type", @@ -228,7 +251,8 @@ def input_blocks_from_frf(frf_path: str) -> dict: input_blocks = input_blocks_from_frf(args.frf) if args.input_bin: - input_blocks = binfile.blocks_from_bin(args.input_bin, flash_info) + bin_info = binfile.blocks_from_bin(args.input_bin, flash_info, secondary_key_path = args.secondary_key) + input_blocks = bin_info.input_blocks logger.info(binfile.input_block_info(input_blocks, flash_info)) # build the dict that's used to proces the blocks @@ -300,7 +324,7 @@ def wrap_callback_function(flasher_step, flasher_status, flasher_progress): if args.output_bin: outfile_data = binfile.bin_from_blocks(output_blocks, flash_info) - Path(args.output_bin).write_bytes(outfile_data) + signature_tools.write_bytes(args.output_bin, outfile_data, signed = args.signed, secondary_key_path = args.secondary_key, boxcode = "", notes = args.notes or "") else: for filename in output_blocks: output_block: BlockData = output_blocks[filename] diff --git a/VW_Flash_GUI.py b/VW_Flash_GUI.py index 50a4464..67cefab 100644 --- a/VW_Flash_GUI.py +++ b/VW_Flash_GUI.py @@ -278,9 +278,10 @@ def on_flash(self, event): self.feedback_text.AppendText( "Extracting Calibration from full binary...\n" ) - input_blocks = binfile.blocks_from_bin( - self.row_obj_dict[selected_file], self.flash_info - ) + + bin_info = binfile.blocks_from_bin(self.row_obj_dict[selected_file], self.flash_info, secondary_key_path = None) + input_blocks = bin_info.input_blocks + # Filter to only CAL block. self.input_blocks = { k: v @@ -338,9 +339,9 @@ def on_flash(self, event): ) self.flash_bin(get_info=False) elif len(input_bytes) == self.flash_info.binfile_size: - self.input_blocks = binfile.blocks_from_bin( - self.row_obj_dict[selected_file], self.flash_info - ) + bin_info = binfile.blocks_from_bin(self.row_obj_dict[selected_file], self.flash_info, secondary_key_path = None) + self.input_blocks = bin_info.input_blocks + self.flash_bin(get_info=False) else: self.feedback_text.AppendText( diff --git a/data/VW_Flash.key b/data/VW_Flash.key new file mode 100644 index 0000000..a4357ff --- /dev/null +++ b/data/VW_Flash.key @@ -0,0 +1,15 @@ +-----BEGIN RSA PRIVATE KEY----- +MIICXAIBAAKBgQCmWzXOqGzr16R+K9h48J/vIxkZO/dp8Gj9zgxEjKHUql5d6+44 +IffHJXXOmFN4f9PnqLy0dZdnzUuHoaWympQMBPG+V3IfL8/fq03hF6FnTK9zBdJL +jk/AME9dhEuJRdycSqr8YuOsaAyHOKOXMkNeP7HxKQrIUcNlJeOPGqfpNQIDAQAB +AoGAAgVdT8+Q/y56o5kXGKUHtF6EnrSxSpquk9gYeGQNpT6VReZ+BDWOw98VzCGN +RgtQ1jbLur+A9t8Hb17h20Zwd6M3++zRBA93urNO4wOYMBoLusroDZWr1lGDLMhb +hMbnBeOhn8rKBbJL66SqW3tFVrDVKdDa9gbGVoIDuX+/W4ECQQC/udRrqalOB5J6 +yrrrcHNEdjkCAHfkH98LgY5tpVg1K5LPuyScQ4F38vC/wqeIS8jDiYsj3zYsgKgC +fcxTa6hBAkEA3iAgijblTRV4iChe+/Li6lIk9r1btimqlXPX66BqDo8FTd0A9gFi +g/iYeyEXAu5TPwE638hOcGJR2D40/N8j9QJBAKHQ5MENjBCIgY/TpVlrKk5A/bJ7 +1LScVbMvYJeYMs+FfD6Jc8fTjeVADQO79YwqckLexqm7Dc0XtTWNGTPbLEECQGv1 +t3sV9VsC3YNoA8p3Idz7seWO4X1nQPbEyCRI4mNTFiPjD62BvM0hzZLC4XlWNnW/ +9kqAA8fRsa/lhEGHfuUCQGoJgwZiLxGrT00xj+iGLZJJ3AyJs5XKw+OZnhT78CwY +JMbNZbkGN77hNiNQAD/tCebFR+6P/u3l3A0bY3yiGlk= +-----END RSA PRIVATE KEY----- \ No newline at end of file diff --git a/data/VW_Flash.pub b/data/VW_Flash.pub new file mode 100644 index 0000000..11a5fb5 --- /dev/null +++ b/data/VW_Flash.pub @@ -0,0 +1,6 @@ +-----BEGIN PUBLIC KEY----- +MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCmWzXOqGzr16R+K9h48J/vIxkZ +O/dp8Gj9zgxEjKHUql5d6+44IffHJXXOmFN4f9PnqLy0dZdnzUuHoaWympQMBPG+ +V3IfL8/fq03hF6FnTK9zBdJLjk/AME9dhEuJRdycSqr8YuOsaAyHOKOXMkNeP7Hx +KQrIUcNlJeOPGqfpNQIDAQAB +-----END PUBLIC KEY----- \ No newline at end of file diff --git a/lib/__init.py__ b/lib/__init.py__ index 016b561..2468d34 100644 --- a/lib/__init.py__ +++ b/lib/__init.py__ @@ -6,3 +6,4 @@ import encrypt import checksum import flash_uds import decryptdsg +import signature_tools diff --git a/lib/binfile.py b/lib/binfile.py index 818e54b..42d8c68 100644 --- a/lib/binfile.py +++ b/lib/binfile.py @@ -2,7 +2,9 @@ from pathlib import Path from .constants import FlashInfo from .constants import BlockData +from .constants import FullBinData from .modules import simosshared +from . import signature_tools logger = logging.getLogger("VWFlash") @@ -119,10 +121,16 @@ def filter_blocks(input_blocks: dict, flash_info: FlashInfo): return input_blocks -def blocks_from_bin(bin_path: str, flash_info: FlashInfo) -> dict: +def blocks_from_bin(bin_path: str, flash_info: FlashInfo, secondary_key_path: str = None) -> dict: + #Read the bin from the file bin_data = Path(bin_path).read_bytes() + + #Run the bin through check_signature_data, it'll return a FullBinData object + bin_info = signature_tools.check_signature_data(bin_data, secondary_key_path = secondary_key_path) + input_blocks = {} + for i in flash_info.block_names_frf.keys(): filename = flash_info.block_names_frf[i] input_blocks[filename] = BlockData( @@ -135,4 +143,7 @@ def blocks_from_bin(bin_path: str, flash_info: FlashInfo) -> dict: input_blocks = filter_blocks(input_blocks, flash_info) - return input_blocks + #set the input_block in the bin_info object to the input_blocks, and return it + bin_info.input_blocks = input_blocks + + return bin_info diff --git a/lib/constants.py b/lib/constants.py index e290583..c6bcbdd 100644 --- a/lib/constants.py +++ b/lib/constants.py @@ -63,6 +63,18 @@ def __init__(self, address, parse_type, description): self.parse_type = parse_type self.description = description +class FullBinData: + input_blocks: dict + metadata: str + valid_signature_one: bool + valid_signature_two: bool + + def __init__(self, input_blocks, metadata, valid_signature_one, valid_signature_two): + self.input_blocks = input_blocks + self.metadata = metadata + self.valid_signature_one = valid_signature_one + self.valid_signature_two = valid_signature_two + class ControlModuleIdentifier: rxid: int diff --git a/lib/signature_tools.py b/lib/signature_tools.py new file mode 100644 index 0000000..4d606db --- /dev/null +++ b/lib/signature_tools.py @@ -0,0 +1,118 @@ +import logging +from Crypto.PublicKey import RSA +from pathlib import Path +from Crypto.Signature.pkcs1_15 import PKCS115_SigScheme +from Crypto.Hash import SHA256 +from . import constants as constants +from .constants import FullBinData + +logger = logging.getLogger("VWFlash") + +VW_Flash_key = constants.internal_path("data", "VW_Flash.key") +VW_Flash_pub = constants.internal_path("data", "VW_Flash.pub") + + +#Build the metadata... basically just make sure the boxcode and the notes aren't too long +def build_metadata(boxcode = "", notes = ""): + metadata = b'METADATA:' + boxcode[0:15].ljust(15,' ').encode("utf-8") + notes[0:70].ljust(70, ' ').encode("utf-8") + return metadata + +#sign binary data using a private key path +def sign_datablock(bin_file, private_key_path): + with open(private_key_path, 'rb') as private_key_file: + private_key = private_key_file.read() + + the_hash = SHA256.new(bin_file) + pkcs115 = PKCS115_SigScheme(RSA.import_key(private_key)) + return pkcs115.sign(the_hash) + +#sign a bin... this will append metadata to a bin, and sign it (optionally twice), then return it +def sign_bin(bin_file, secondary_key_path = None, boxcode = "", notes = ""): + metadata = build_metadata(boxcode = boxcode, notes = notes) + bin_file += metadata + + signature1 = sign_datablock(bin_file, VW_Flash_key) + + if secondary_key_path: + signature2 = sign_datablock(bin_file, secondary_key_path) + else: + signature2 = signature1 + + signed_file = bin_file + signature1 + signature2 + + return signed_file + +#Verify a bin +def verify_bin(bin_file, signature, public_key_path): + with open(public_key_path, 'rb') as public_key_file: + public_key = public_key_file.read() + + + the_hash = SHA256.new(bin_file) + pkcs115 = PKCS115_SigScheme(RSA.import_key(public_key)) + + try: + verified = pkcs115.verify(the_hash, signature) + return True + except: + return False + +#write_bytes function... used to replace write_bytes throughout the code so it can be handled in a more +#centralized way +def write_bytes(outfile, binary_data, signed = False, secondary_key_path = None, boxcode = "", notes = ""): + #Default, just write out to the file_path as bytes: + if signed: + binary_data = sign_bin(bin_file = binary_data, secondary_key_path = secondary_key_path, boxcode = boxcode, notes = notes) + + Path(outfile).write_bytes(binary_data) + + +#read_bytes function... used to replace read_bytes throughout the code so it can be handled in a more +#centralized way +def check_signature_data(bin_data, secondary_key_path = None): + + valid_signature_one = False + valid_signature_two = False + metadata = None + + #Check if there's metadata and signature(s) at the end of the file: + sig_block = bin_data[-350:] + if sig_block[0:9] == b'METADATA:': + logger.info("Found signature block in bin file, validating") + #Print out the metadata that's included in the file + metadata = str(sig_block[0:-256]) + + logger.info(str(sig_block[0:-256])) + + #Pull the signatures out + signature1 = sig_block[-256:-128] + signature2 = sig_block[-128:] + + #Validate the first signature using the VW_Flash public key + if verify_bin(bin_data[0:-256], signature1, VW_Flash_pub): + logger.info("First signature validated") + valid_signature_one = True + else: + logger.critical("First signature failed! File has been modified!") + + #if the signatures are the same, there's no point checking the second one, just continue on + if signature1 == signature2: + logger.info("No secondary signature found") + + elif secondary_key_path: + if verify_bin(bin_data[0:-256], signature2, secondary_key_path): + logger.info("Second signature validated") + valid_signature_two = True + else: + logger.critical("Second signature failed!") + + else: + logger.info("File contains additional signature, but no public key arg provided") + + #Pull the signature block off the end of the bin file so we can process it by itself + bin_data = bin_data[0:-350] + + + return FullBinData({"full_bin": bin_data}, metadata, valid_signature_one, valid_signature_two) + +