def test_cert_block_invalid(): cb = CertBlockV2() cb.RKH_SIZE = 77777 with pytest.raises(SPSDKError, match="Invalid length of data"): cb.rkht with pytest.raises(SPSDKError, match="Invalid image length"): cb.image_length = -2 with pytest.raises(SPSDKError, match="Invalid alignment"): cb.alignment = -2 cb = CertBlockV2() with pytest.raises(SPSDKError, match="Invalid index of root key hash in the table"): cb.set_root_key_hash(5, bytes(32)) with pytest.raises(SPSDKError, match="Invalid length of key hash"): cb.set_root_key_hash(3, bytes(5))
def certificate_block(data_dir, der_file_names, index=0, chain_der_file_names=None) -> CertBlockV2: """ :param data_dir: absolute path of data dir where the test keys are located :param der_file_names: list of filenames of the DER certificate :param index: of the root certificate (index to der_file_names list) :param chain_der_file_names: list of filenames of der certificates in chain (applied for `index`) :return: certificate block for testing """ # read public certificate cert_data_list = list() for der_file_name in der_file_names: if der_file_name: with open(os.path.join(data_dir, "keys_and_certs", der_file_name), "rb") as f: cert_data_list.append(f.read()) else: cert_data_list.append(None) # create certification block cert_block = CertBlockV2(build_number=1) cert_block.add_certificate(cert_data_list[index]) if chain_der_file_names: for der_file_name in chain_der_file_names: with open(os.path.join(data_dir, "keys_and_certs", der_file_name), "rb") as f: cert_block.add_certificate(f.read()) # add hashes for root_key_index, cert_data in enumerate(cert_data_list): if cert_data: cert_block.set_root_key_hash(root_key_index, Certificate(cert_data)) cert_block.export() # check export works cert_block.info() # check info works return cert_block
def test_cert_block(data_dir): with open(os.path.join(data_dir, 'selfsign_2048_v3.der.crt'), 'rb') as f: cert_data = f.read() cert_obj = Certificate(cert_data) cb = CertBlockV2() cb.set_root_key_hash(0, cert_obj.public_key_hash) cb.add_certificate(cert_data) assert cb.rkh_index == 0 cb.export() # test RKHT assert cb.rkht.hex( ) == 'db31d46c717711a8231cbc38b1de8a6e8657e1f733e04c2ee4b62fcea59149fa' fuses = cb.rkht_fuses assert len(fuses) == 8 assert fuses[0] == 1825845723 # test exception if child certificate in chain is not signed by parent certificate with open(os.path.join(data_dir, 'ca0_v3.der.crt'), 'rb') as f: ca0_cert_data = f.read() ca0_cert = Certificate(ca0_cert_data) with pytest.raises(ValueError): cb.add_certificate(ca0_cert) # test exception if no certificate specified cb = CertBlockV2() cb.set_root_key_hash(0, cert_obj.public_key_hash) with pytest.raises(ValueError): cb.export() # test exception last certificate is set as CA cb = CertBlockV2() cb.set_root_key_hash(0, ca0_cert.public_key_hash) cb.add_certificate(ca0_cert) with pytest.raises(ValueError): cb.export() # test exception if hash does not match any certificate cb = CertBlockV2() cb.set_root_key_hash(0, ca0_cert.public_key_hash) cb.add_certificate(cert_data) with pytest.raises(ValueError): cb.export()
def _create_cert_block_v2(data_dir: str) -> CertBlockV2: with open(os.path.join(data_dir, 'selfsign_v3.der.crt'), 'rb') as f: cert_data = f.read() cb = CertBlockV2() cert_obj = Certificate(cert_data) cb.set_root_key_hash(0, cert_obj.public_key_hash) cb.add_certificate(cert_obj) return cb
def parse( cls, data: bytes, offset: int = 0, dek: bytes = b"", mac: bytes = b"", counter: Optional[Counter] = None, ) -> "CertSectionV2": """Parse Certificate Section from bytes array. :param data: Raw data of parsed image :param offset: The offset of input data :param dek: The DEK value in bytes (required) :param mac: The MAC value in bytes (required) :param counter: The counter object (required) :return: parsed cert section v2 object :raises SPSDKError: Raised when dek, mac, counter are not valid :raises SPSDKError: Raised when there is invalid header HMAC, TAG, FLAGS, Mark :raises SPSDKError: Raised when there is invalid certificate block HMAC """ if not isinstance(dek, bytes): raise SPSDKError("DEK value has invalid format") if not isinstance(mac, bytes): raise SPSDKError("MAC value has invalid format") if not isinstance(counter, Counter): raise SPSDKError("Counter value has invalid format") index = offset header_encrypted = data[index:index + CmdHeader.SIZE] index += CmdHeader.SIZE header_hmac = data[index:index + cls.HMAC_SIZE] index += cls.HMAC_SIZE cert_block_hmac = data[index:index + cls.HMAC_SIZE] index += cls.HMAC_SIZE if header_hmac != crypto_backend().hmac(mac, header_encrypted): raise SPSDKError("Invalid Header HMAC") header_encrypted = crypto_backend().aes_ctr_decrypt( dek, header_encrypted, counter.value) header = CmdHeader.parse(header_encrypted) if header.tag != EnumCmdTag.TAG: raise SPSDKError(f"Invalid Header TAG: 0x{header.tag:02X}") if header.flags != (EnumSectionFlag.CLEARTEXT | EnumSectionFlag.LAST_SECT): raise SPSDKError(f"Invalid Header FLAGS: 0x{header.flags:02X}") if header.address != cls.SECT_MARK: raise SPSDKError(f"Invalid Section Mark: 0x{header.address:08X}") # Parse Certificate Block cert_block = CertBlockV2.parse(data, index) if cert_block_hmac != crypto_backend().hmac( mac, data[index:index + cert_block.raw_size]): raise SPSDKError("Invalid Certificate Block HMAC") index += cert_block.raw_size cert_section_obj = cls(cert_block) counter.increment(calc_cypher_block_count(index - offset)) return cert_section_obj
def gen_cert_block() -> CertBlockV2: """Generate a Certification Block.""" with open(f"{DATA_DIR}/selfsign_v3.der.crt", "rb") as cert_file: cert_data = cert_file.read() cert_obj = Certificate(cert_data) root_key = cert_obj.public_key_hash cert_block = CertBlockV2() cert_block.set_root_key_hash(0, root_key) cert_block.add_certificate(cert_data) return cert_block
def test_add_invalid_cert_in_cert_block(data_dir): cb = CertBlockV2() with open(os.path.join(data_dir, "selfsign_2048_v3.der.crt"), "rb") as f: cert_data = f.read() with open(os.path.join(data_dir, "ca0_v3.der.crt"), "rb") as f: ca0_cert_data = f.read() with pytest.raises(SPSDKError): cb.add_certificate(cert=5) with pytest.raises( SPSDKError, match="Chain certificate cannot be verified using parent public key" ): cb.add_certificate(cert=cert_data) cb.add_certificate(cert=ca0_cert_data)
def test_cert_block_export_invalid(data_dir): with open(os.path.join(data_dir, "selfsign_2048_v3.der.crt"), "rb") as f: cert_data = f.read() with open(os.path.join(data_dir, "ca0_v3.der.crt"), "rb") as f: ca0_cert_data = f.read() cert_obj = Certificate(cert_data) cb = CertBlockV2() cb.set_root_key_hash(0, cert_obj.public_key_hash) cb.add_certificate(cert_data) cb.add_certificate(cert_data) assert cb.rkh_index == 0 with pytest.raises( SPSDKError, match="All certificates except the last chain certificate must be CA" ): cb.export()
def test_cert_block_basic(): cb = CertBlockV2() # test default values assert cb.image_length == 0 assert cb.alignment == 16 assert cb.rkh_index is None # test setters cb.image_length = 1 cb.alignment = 1 assert cb.alignment == 1 assert cb.image_length == 1 assert cb.header.image_length == 1 # invalid root key index with pytest.raises(AssertionError): cb.set_root_key_hash(4, bytes([0] * 32)) # invalid root key size with pytest.raises(AssertionError): cb.set_root_key_hash(0, bytes())
def parse( cls, data: bytes, offset: int = 0, kek: bytes = bytes(), plain_sections: bool = False, ) -> "BootImageV21": """Parse image from bytes. :param data: Raw data of parsed image :param offset: The offset of input data :param kek: The Key for unwrapping DEK and MAC keys (required) :param plain_sections: Sections are not encrypted; this is used only for debugging, not supported by ROM code :return: BootImageV21 parsed object :raises Exception: raised when header is in incorrect format :raises Exception: raised when signature is incorrect :raises SPSDKError: Raised when kek is empty :raises Exception: raised when header's nonce not present" """ if not kek: raise SPSDKError("kek cannot be empty") index = offset header_raw_data = data[index:index + ImageHeaderV2.SIZE] index += ImageHeaderV2.SIZE # TODO not used right now: hmac_data = data[index: index + cls.HEADER_MAC_SIZE] index += cls.HEADER_MAC_SIZE key_blob = data[index:index + cls.KEY_BLOB_SIZE] index += cls.KEY_BLOB_SIZE key_blob_unwrap = crypto_backend().aes_key_unwrap(kek, key_blob[:-8]) dek = key_blob_unwrap[:32] mac = key_blob_unwrap[32:] # Parse Header header = ImageHeaderV2.parse(header_raw_data) if header.offset_to_certificate_block != (index - offset): raise Exception() # Parse Certificate Block cert_block = CertBlockV2.parse(data, index) index += cert_block.raw_size # Verify Signature signature_index = index # The image may containt SHA, in such a case the signature is placed # after SHA. Thus we must shift the index by SHA size. if header.flags & BootImageV21.FLAGS_SHA_PRESENT_BIT: signature_index += BootImageV21.SHA_256_SIZE result = cert_block.verify_data( data[signature_index:signature_index + cert_block.signature_size], data[offset:signature_index], ) if not result: raise Exception() # Check flags, if 0x8000 bit is set, the SB file contains SHA-256 between # certificate and signature. if header.flags & BootImageV21.FLAGS_SHA_PRESENT_BIT: bootable_section_sha256 = data[index:index + BootImageV21.SHA_256_SIZE] index += BootImageV21.SHA_256_SIZE index += cert_block.signature_size # Check first Boot Section HMAC # TODO: not implemented yet # hmac_data_calc = crypto_backend().hmac(mac, data[index + CmdHeader.SIZE: index + CmdHeader.SIZE + ((2) * 32)]) # if hmac_data != hmac_data_calc: # raise Exception() if not header.nonce: raise SPSDKError("Header's nonce not present") counter = Counter(header.nonce) counter.increment(calc_cypher_block_count(index - offset)) boot_section = BootSectionV2.parse(data, index, dek=dek, mac=mac, counter=counter, plain_sect=plain_sections) if header.flags & BootImageV21.FLAGS_SHA_PRESENT_BIT: computed_bootable_section_sha256 = internal_backend.hash( data[index:], algorithm="sha256") if bootable_section_sha256 != computed_bootable_section_sha256: raise SPSDKError(desc=( "Error: invalid Bootable section SHA." f"Expected {bootable_section_sha256.decode('utf-8')}," f"got {computed_bootable_section_sha256.decode('utf-8')}")) adv_params = SBV2xAdvancedParams(dek=dek, mac=mac, nonce=header.nonce, timestamp=header.timestamp) obj = cls( kek=kek, product_version=str(header.product_version), component_version=str(header.component_version), build_number=header.build_number, advanced_params=adv_params, ) obj.cert_block = cert_block obj.add_boot_section(boot_section) return obj
def parse(cls, data: bytes, offset: int = 0, kek: bytes = bytes(), plain_sections: bool = False) -> 'BootImageV21': """Parse image from bytes. :param data: Raw data of parsed image :param offset: The offset of input data :param kek: The Key for unwrapping DEK and MAC keys (required) :param plain_sections: Sections are not encrypted; this is used only for debugging, not supported by ROM code :return: BootImageV21 parsed object :raise Exception: raised when header is in incorrect format :raise Exception: raised when signature is incorrect """ assert kek, 'kek cannot be empty' index = offset header_raw_data = data[index:index + ImageHeaderV2.SIZE] index += ImageHeaderV2.SIZE # TODO not used right now: hmac_data = data[index: index + cls.HEADER_MAC_SIZE] index += cls.HEADER_MAC_SIZE key_blob = data[index:index + cls.KEY_BLOB_SIZE] index += cls.KEY_BLOB_SIZE key_blob_unwrap = crypto_backend().aes_key_unwrap(kek, key_blob[:-8]) dek = key_blob_unwrap[:32] mac = key_blob_unwrap[32:] # Parse Header header = ImageHeaderV2.parse(header_raw_data) if header.offset_to_certificate_block != (index - offset): raise Exception() # Parse Certificate Block cert_block = CertBlockV2.parse(data, index) index += cert_block.raw_size # Verify Signature if not cert_block.verify_data( data[index:index + cert_block.signature_size], data[offset:index]): raise Exception() index += cert_block.signature_size # Check first Boot Section HMAC # TODO: not implemented yet # hmac_data_calc = crypto_backend().hmac(mac, data[index + CmdHeader.SIZE: index + CmdHeader.SIZE + ((2) * 32)]) # if hmac_data != hmac_data_calc: # raise Exception() assert header.nonce counter = Counter(header.nonce) counter.increment(calc_cypher_block_count(index - offset)) boot_section = BootSectionV2.parse(data, index, dek=dek, mac=mac, counter=counter, plain_sect=plain_sections) adv_params = SBV2xAdvancedParams(dek=dek, mac=mac, nonce=header.nonce, timestamp=header.timestamp) obj = cls(kek=kek, product_version=str(header.product_version), component_version=str(header.component_version), build_number=header.build_number, advanced_params=adv_params) obj.cert_block = cert_block obj.add_boot_section(boot_section) return obj
def generate_secure_binary_21( bd_file_path: click.Path, output_file_path: click.Path, key_file_path: click.Path, private_key_file_path: click.Path, signing_certificate_file_paths: List[click.Path], root_key_certificate_paths: List[click.Path], hoh_out_path: click.Path, external_files: List[click.Path], ) -> None: """Generate SecureBinary image from BD command file. :param bd_file_path: path to BD file. :param output_file_path: output path to generated secure binary file. :param key_file_path: path to key file. :param private_key_file_path: path to private key file for signing. This key relates to last certificate from signing certificate chain. :param signing_certificate_file_paths: signing certificate chain. :param root_key_certificate_paths: paths to root key certificate(s) for verifying other certificates. Only 4 root key certificates are allowed, others are ignored. One of the certificates must match the first certificate passed in signing_certificate_file_paths. :param hoh_out_path: output path to hash of hashes of root keys. If set to None, 'hash.bin' is created under working directory. :param external_files: external files referenced from BD file. :raises SPSDKError: If incorrect bf file is provided """ # Create lexer and parser, load the BD file content and parse it for # further execution - the parsed BD file is a dictionary in JSON format with open(str(bd_file_path)) as bd_file: bd_file_content = bd_file.read() parser = bd_parser.BDParser() parsed_bd_file = parser.parse(text=bd_file_content, extern=external_files) if parsed_bd_file is None: raise SPSDKError( "Invalid bd file, secure binary file generation terminated") # The dictionary contains following content: # { # options: { # opt1: value,... # }, # sections: [ # {section_id: value, options: {}, commands: {}}, # {section_id: value, options: {}, commands: {}} # ] # } # TODO check, that section_ids differ in sections??? # we need to encrypt and sign the image, let's check, whether we have # everything we need # It appears, that flags option in BD file are irrelevant for 2.1 secure # binary images regarding encryption/signing - SB 2.1 must be encrypted # and signed. # However, bit 15 represents, whether the final SB 2.1 must include a # SHA-256 of the botable section. flags = parsed_bd_file["options"].get( "flags", BootImageV21.FLAGS_SHA_PRESENT_BIT | BootImageV21.FLAGS_ENCRYPTED_SIGNED_BIT) if (private_key_file_path is None or signing_certificate_file_paths is None or root_key_certificate_paths is None): click.echo( "error: Signed image requires private key with -s option, " "one or more certificate(s) using -S option and one or more root key " "certificates using -R option") sys.exit(1) # Versions and build number are up to the user. If he doesn't provide any, # we set these to following values. product_version = parsed_bd_file["options"].get("productVersion", "") component_version = parsed_bd_file["options"].get("componentVersion", "") build_number = parsed_bd_file["options"].get("buildNumber", -1) if not product_version: product_version = "1.0.0" click.echo( "warning: production version not defined, defaults to '1.0.0'") if not component_version: component_version = "1.0.0" click.echo( "warning: component version not defined, defaults to '1.0.0'") if build_number == -1: build_number = 1 click.echo("warning: build number not defined, defaults to '1.0.0'") if key_file_path is None: # Legacy elf2sb doesn't report no key provided, but this should # be definitely reported to tell the user, what kind of key is being # used click.echo("warning: no KEK key provided, using a zero KEK key") sb_kek = bytes.fromhex("0" * 64) else: with open(str(key_file_path)) as kek_key_file: # TODO maybe we should validate the key length and content, to make # sure the key provided in the file is valid?? sb_kek = bytes.fromhex(kek_key_file.readline()) # validate keyblobs and perform appropriate actions keyblobs = parsed_bd_file.get("keyblobs", []) # Based on content of parsed BD file, create a BootSectionV2 and assign # commands to them. # The content of section looks like this: # sections: [ # { # section_id: <number>, # options: {}, this is left empty for now... # commands: [ # {<cmd1>: {<param1>: value, ...}}, # {<cmd2>: {<param1>: value, ...}}, # ... # ] # }, # { # section_id: <number>, # ... # } # ] sb_sections = [] bd_sections = parsed_bd_file["sections"] for bd_section in bd_sections: section_id = bd_section["section_id"] commands = [] for cmd in bd_section["commands"]: for key, value in cmd.items(): # we use a helper function, based on the key ('load', 'erase' # etc.) to create a command object. The helper function knows # how to handle the parameters of each command. # TODO Only load, fill, erase and enable commands are supported # for now. But there are few more to be supported... cmd_fce = elf2sb_helper21.get_command(key) if key in ("keywrap", "encrypt"): keyblob = {"keyblobs": keyblobs} value.update(keyblob) cmd = cmd_fce(value) commands.append(cmd) sb_sections.append(BootSectionV2(section_id, *commands)) # We have a list of sections and their respective commands, lets create # a boot image v2.1 object secure_binary = BootImageV21( sb_kek, *sb_sections, product_version=product_version, component_version=component_version, build_number=build_number, flags=flags, ) # create certificate block cert_block = CertBlockV2(build_number=build_number) for cert_path in signing_certificate_file_paths: cert_data = load_certificate_as_bytes(str(cert_path)) cert_block.add_certificate(cert_data) for cert_idx, cert_path in enumerate(root_key_certificate_paths): cert_data = load_certificate_as_bytes(str(cert_path)) cert_block.set_root_key_hash(cert_idx, Certificate(cert_data)) # We have our secure binary, now we attach to it the certificate block and # the private key content # TODO legacy elf2sb doesn't require you to use certificates and private key, # so maybe we should make sure this is not necessary??? # The -s/-R/-S are mandatory, 2.0 format not supported!!! secure_binary.cert_block = cert_block secure_binary.private_key_pem_data = load_binary( str(private_key_file_path)) if hoh_out_path is None: hoh_out_path = os.path.join(os.getcwd(), "hash.bin") with open(str(hoh_out_path), "wb") as rkht_file: rkht_file.write(secure_binary.cert_block.rkht) with open(str(output_file_path), "wb") as sb_file_output: sb_file_output.write(secure_binary.export()) click.echo(f"Success. (Secure binary 2.1: {output_file_path} created.)")