def test_counter(): """Test of Counter.""" # simple counter with nonce only cntr = Counter(bytes([0] * 16)) assert cntr.value == bytes([0] * 16) # counter with nonce and counter encoded as little endian cntr = Counter(bytes([0] * 16), ctr_value=0x01234567, ctr_byteorder_encoding="little") assert cntr.value == bytes([0] * 12 + [0x67, 0x45, 0x23, 0x01]) # counter with nonce and counter encoded as little endian cntr = Counter(bytes([0] * 16), ctr_value=0x01234567) assert cntr.value == bytes([0] * 12 + [0x67, 0x45, 0x23, 0x01]) # counter with nonce and counter encoded as big endian cntr = Counter(bytes([0] * 16), ctr_value=1, ctr_byteorder_encoding="big") assert cntr.value == bytes([0] * 15 + [1]) # increment cntr.increment() assert cntr.value == bytes([0] * 15 + [2]) cntr.increment(2) assert cntr.value == bytes([0] * 15 + [4]) cntr.increment(256) assert cntr.value == bytes([0] * 14 + [1, 4])
def test_invalid_header_hmac(data_dir): cs = CertSectionV2(_create_cert_block_v2(data_dir)) dek = crypto_backend().random_bytes(32) mac = crypto_backend().random_bytes(32) nonce = crypto_backend().random_bytes(16) valid_data = cs.export(dek, mac, Counter(nonce)) invalid_data = valid_data invalid_data = bytearray(invalid_data) invalid_data[0:32] = bytearray(32) with pytest.raises(SPSDKError, match="HMAC"): CertSectionV2.parse(invalid_data, 0, dek, mac, Counter(nonce))
def test_invalid_header_flag(data_dir): cs = CertSectionV2(_create_cert_block_v2(data_dir)) cs._header.address += 1 dek = crypto_backend().random_bytes(32) mac = crypto_backend().random_bytes(32) nonce = crypto_backend().random_bytes(16) valid_data = cs.export(dek, mac, Counter(nonce)) with pytest.raises(SPSDKError, match="Mark"): CertSectionV2.parse(data=valid_data, mac=mac, dek=dek, counter=Counter(nonce))
def test_certificate_section_v2(data_dir: str) -> None: with pytest.raises(AssertionError): CertSectionV2(None) cs = CertSectionV2(_create_cert_block_v2(data_dir)) dek = crypto_backend().random_bytes(32) mac = crypto_backend().random_bytes(32) nonce = crypto_backend().random_bytes(16) data = cs.export(dek, mac, Counter(nonce)) assert data assert CertSectionV2.parse(data, 0, dek, mac, Counter(nonce)) with pytest.raises(Exception): CertSectionV2.parse(data, 0, dek, crypto_backend().random_bytes(32), Counter(nonce))
def encrypt_block(self, key: bytes, start_addr: int, data: bytes) -> bytes: """Encrypt block located in any FAC region. :param key: user for encryption :param start_addr: start address of the data :param data: binary block to be encrypted; the block size must be BEE_ENCR_BLOCK_SIZE :return: encrypted block if it is inside any FAC region; untouched block if it is not in any FAC region :raises SPSDKError: When incorrect length of binary block :raises SPSDKError: When encryption mode different from AES/CTR provided :raises SPSDKError: When invalid length of key :raises SPSDKError: When invalid range of region """ if len(data) != BEE_ENCR_BLOCK_SIZE: raise SPSDKError( "Incorrect length of binary block to be encrypted") if self._start_addr <= start_addr < self._end_addr: if self.mode != BeeProtectRegionBlockAesMode.CTR: raise SPSDKError("only AES/CTR encryption mode supported now") if len(key) != 16: raise SPSDKError("Invalid length of key") for fac in self.fac_regions: if fac.start_addr <= start_addr < fac.end_addr: if start_addr + len(data) > fac.end_addr: raise SPSDKError("Invalid range of region") cntr_key = Counter( self.counter, ctr_value=start_addr >> 4, ctr_byteorder_encoding="big", ) return crypto_backend().aes_ctr_encrypt( key, data, cntr_key.value) return data
def test_boot_section_v2(): boot_section = BootSectionV2(0, CmdErase(address=0, length=100000), CmdLoad(address=0, data=b"0123456789"), CmdReset()) assert boot_section.uid == 0 assert not boot_section.is_last assert boot_section.hmac_count == 1 assert boot_section.raw_size == 144 dek = crypto_backend().random_bytes(32) mac = crypto_backend().random_bytes(32) nonce = crypto_backend().random_bytes(16) data = boot_section.export(dek, mac, Counter(nonce)) assert data assert BootSectionV2.parse(data, 0, False, dek, mac, Counter(nonce)) with pytest.raises(SPSDKError, match="Invalid type of dek, should be bytes"): BootSectionV2.parse(data=data, offset=0, plain_sect=False, dek=4, mac=mac, counter=Counter(nonce)) with pytest.raises(SPSDKError, match="Invalid type of mac, should be bytes"): BootSectionV2.parse(data=data, offset=0, plain_sect=False, dek=dek, mac=4, counter=Counter(nonce)) with pytest.raises(SPSDKError, match="Invalid type of counter"): BootSectionV2.parse(data=data, offset=0, plain_sect=False, dek=dek, mac=mac, counter=5) with pytest.raises(SPSDKError): assert BootSectionV2.parse(data, 0, False, dek, crypto_backend().random_bytes(32), Counter(nonce))
def test_invalid_export_cert_section_v2(data_dir): cs = CertSectionV2(_create_cert_block_v2(data_dir)) dek = crypto_backend().random_bytes(16) mac = crypto_backend().random_bytes(16) nonce = crypto_backend().random_bytes(16) cs.HMAC_SIZE = 137 with pytest.raises(SPSDKError, match="Invalid size"): cs.export(dek, mac, Counter(nonce))
def export(self, padding: Optional[bytes] = None) -> bytes: """Serialize image object. :param padding: header padding (8 bytes) for testing purpose; None to use random values (recommended) :return: exported bytes :raises SPSDKError: Raised when there are no boot sections or is not signed or private keys are missing :raises SPSDKError: Raised when there is invalid dek or mac :raises SPSDKError: Raised when certificate data is not present :raises SPSDKError: Raised when there is invalid certificate block :raises SPSDKError: Raised when there is invalid length of exported data """ if len(self.dek) != 32 or len(self.mac) != 32: raise SPSDKError("Invalid dek or mac") # validate params if not self._boot_sections: raise SPSDKError("No boot section") if self.signed and (self._cert_section is None): raise SPSDKError( "Certificate section is required for signed images") # update internals self.update() # Add Image Header data data = self._header.export(padding=padding) # Add Image Header HMAC data data += crypto_backend().hmac(self.mac, data) # Add DEK and MAC keys data += crypto_backend().aes_key_wrap(self.kek, self.dek + self.mac) # Add Padding data += padding if padding else crypto_backend().random_bytes(8) # Add Certificates data if not self._header.nonce: raise SPSDKError("There is no nonce in the header") counter = Counter(self._header.nonce) counter.increment(calc_cypher_block_count(len(data))) if self._cert_section is not None: cert_sect_bin = self._cert_section.export(dek=self.dek, mac=self.mac, counter=counter) counter.increment(calc_cypher_block_count(len(cert_sect_bin))) data += cert_sect_bin # Add Boot Sections data for sect in self._boot_sections: data += sect.export(dek=self.dek, mac=self.mac, counter=counter) # Add Signature data if self.signed: private_key_pem_data = self.private_key_pem_data if private_key_pem_data is None: raise SPSDKError( "Private key not assigned, cannot sign the image") certificate_block = self.cert_block if not ((certificate_block is not None) and certificate_block. verify_private_key(private_key_pem_data)): raise SPSDKError("Invalid certificate block") data += crypto_backend().rsa_sign(private_key_pem_data, data) if len(data) != self.raw_size: raise SPSDKError("Invalid length of exported data") return data
def test_boot_section_v2_invalid_export(): boot_section = BootSectionV2(0, CmdErase(address=0, length=100000), CmdLoad(address=0, data=b"0123456789"), CmdReset()) dek = 32 mac = 4 nonce = crypto_backend().random_bytes(16) with pytest.raises(SPSDKError, match="Invalid type of dek, should be bytes"): boot_section.export(dek, mac, Counter(nonce)) dek = crypto_backend().random_bytes(32) with pytest.raises(SPSDKError, match="Invalid type of mac, should be bytes"): boot_section.export(dek, mac, Counter(nonce)) counter = 5 mac = crypto_backend().random_bytes(32) with pytest.raises(SPSDKError, match="Invalid type of counter"): boot_section.export(dek, mac, counter)
def test_boot_section_v2(): boot_section = BootSectionV2(0, CmdErase(address=0, length=100000), CmdLoad(address=0, data=b'0123456789'), CmdReset()) assert boot_section.uid == 0 assert not boot_section.is_last assert boot_section.hmac_count == 1 assert boot_section.raw_size == 144 dek = crypto_backend().random_bytes(32) mac = crypto_backend().random_bytes(32) nonce = crypto_backend().random_bytes(16) data = boot_section.export(dek, mac, Counter(nonce)) assert data assert BootSectionV2.parse(data, 0, False, dek, mac, Counter(nonce)) with pytest.raises(Exception): assert BootSectionV2.parse(data, 0, False, dek, crypto_backend().random_bytes(32), Counter(nonce))
def encrypt_block(self, key: bytes, start_addr: int, data: bytes) -> bytes: """Encrypt block located in any FAC region. :param key: user for encryption :param start_addr: start address of the data :param data: binary block to be encrypted; the block size must be BEE_ENCR_BLOCK_SIZE :return: encrypted block if it is inside any FAC region; untouched block if it is not in any FAC region """ assert len(data) == BEE_ENCR_BLOCK_SIZE if self._start_addr <= start_addr < self._end_addr: assert self.mode == BeeProtectRegionBlockAesMode.CTR, 'only AES/CTR encryption mode supported now' assert len(key) == 16 for fac in self.fac_regions: if fac.start_addr <= start_addr < fac.end_addr: assert start_addr + len(data) <= fac.end_addr cntr_key = Counter(self.counter, ctr_value=start_addr >> 4, ctr_byteorder_encoding='big') return crypto_backend().aes_ctr_encrypt( key, data, cntr_key.value) return data
def parse(cls, data: bytes, offset: int = 0, kek: bytes = bytes()) -> "BootImageV20": """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) :return: parsed image object :raise Exception: raised when header is in wrong format :raise Exception: raised when there is invalid header version :raise Exception: raised when signature is incorrect :raises SPSDKError: Raised when kek is empty :raises Exception: raised when header's nonce is not present """ if not kek: raise SPSDKError("kek cannot be empty") index = offset header_raw_data = data[index:index + ImageHeaderV2.SIZE] index += ImageHeaderV2.SIZE header_mac_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:] header_mac_data_calc = crypto_backend().hmac(mac, header_raw_data) if header_mac_data != header_mac_data_calc: raise Exception() # Parse Header header = ImageHeaderV2.parse(header_raw_data) if header.version != "2.0": raise Exception( f"Invalid Header Version: {header.version} instead 2.0") image_size = header.image_blocks * 16 # Initialize counter if not header.nonce: raise SPSDKError("Header's nonce not present") counter = Counter(header.nonce) counter.increment(calc_cypher_block_count(index - offset)) # ... signed = header.flags == 0x08 adv_params = SBV2xAdvancedParams(dek=dek, mac=mac, nonce=header.nonce, timestamp=header.timestamp) obj = cls( signed, kek=kek, product_version=str(header.product_version), component_version=str(header.component_version), build_number=header.build_number, advanced_params=adv_params, ) # Parse Certificate section if header.flags == 0x08: cert_sect = CertSectionV2.parse(data, index, dek=dek, mac=mac, counter=counter) obj._cert_section = cert_sect index += cert_sect.raw_size # Check Signature if not cert_sect.cert_block.verify_data( data[offset + image_size:], data[offset:offset + image_size]): raise Exception() # Parse Boot Sections while index < (image_size + offset): boot_section = BootSectionV2.parse(data, index, dek=dek, mac=mac, counter=counter) obj.add_boot_section(boot_section) index += boot_section.raw_size return obj
def test_counter_invalid(): with pytest.raises(SPSDKError, match="Wrong byte order"): Counter(nonce=bytes(16), ctr_byteorder_encoding="BIG")
def export(self, padding: Optional[bytes] = None, dbg_info: Optional[List[str]] = None) -> bytes: """Serialize image object. :param padding: header padding (8 bytes) for testing purpose; None to use random values (recommended) :param dbg_info: optional list, where debug info is exported in text form :return: exported bytes :raise ValueError: raised when there is no boot section to be added :raise ValueError: raise when certificate is not assigned :raise ValueError: raise when private key is not assigned """ # validate params if not self._boot_sections: raise ValueError("At least one Boot Section must be added") if self.cert_block is None: raise ValueError('Certificate is not assigned') if self.private_key_pem_data is None: raise ValueError('Private key not assigned, cannot sign the image') # Update internals if dbg_info is not None: dbg_info.append('[sb_file]') bs_dbg_info: Optional[List[str]] = list() if dbg_info else None self.update() # Export Boot Sections bs_data = bytes() # TODO: implement helper method for get key size in bytes. Now is working only with internal backend bs_offset = (ImageHeaderV2.SIZE + self.HEADER_MAC_SIZE + self.KEY_BLOB_SIZE + self.cert_block.raw_size + self.cert_block.signature_size) assert self._header.nonce counter = Counter(self._header.nonce, calc_cypher_block_count(bs_offset)) for sect in self._boot_sections: bs_data += sect.export(dek=self.dek, mac=self.mac, counter=counter, dbg_info=bs_dbg_info) # Export Header signed_data = self._header.export(padding=padding) if dbg_info: dbg_info.append('[header]') dbg_info.append(signed_data.hex()) # Add HMAC data first_bs_hmac_count = self._boot_sections[0].hmac_count hmac_data = bs_data[CmdHeader.SIZE:CmdHeader.SIZE + (first_bs_hmac_count * 32) + 32] hmac = crypto_backend().hmac(self.mac, hmac_data) signed_data += hmac if dbg_info: dbg_info.append('[hmac]') dbg_info.append(hmac.hex()) # Add KeyBlob data key_blob = crypto_backend().aes_key_wrap(self.kek, self.dek + self.mac) key_blob += b'\00' * (self.KEY_BLOB_SIZE - len(key_blob)) signed_data += key_blob if dbg_info: dbg_info.append('[key_blob]') dbg_info.append(key_blob.hex()) # Add Certificates data signed_data += self.cert_block.export() if dbg_info: dbg_info.append('[cert_block]') dbg_info.append(self.cert_block.export().hex()) # Add Signature data assert self.cert_block.verify_private_key( self.private_key_pem_data ) # verify private key matches certificate signature = crypto_backend().rsa_sign(self.private_key_pem_data, signed_data) if dbg_info: dbg_info.append('[signature]') dbg_info.append(signature.hex()) dbg_info.append('[boot_sections]') assert bs_dbg_info dbg_info.extend(bs_dbg_info) return signed_data + signature + bs_data
def export(self, padding: Optional[bytes] = None, dbg_info: Optional[List[str]] = None) -> bytes: """Serialize image object. :param padding: header padding (8 bytes) for testing purpose; None to use random values (recommended) :param dbg_info: optional list, where debug info is exported in text form :return: exported bytes :raises SPSDKError: Raised when there is no boot section to be added :raises SPSDKError: Raised when certificate is not assigned :raises SPSDKError: Raised when private key is not assigned :raises SPSDKError: Raised when private header's nonce is invalid :raises SPSDKError: Raised when private key does not match certificate :raises SPSDKError: Raised when there is no debug info """ # validate params if not self._boot_sections: raise SPSDKError("At least one Boot Section must be added") if self.cert_block is None: raise SPSDKError("Certificate is not assigned") if self.private_key_pem_data is None: raise SPSDKError("Private key not assigned, cannot sign the image") # Update internals if dbg_info is not None: dbg_info.append("[sb_file]") bs_dbg_info: Optional[List[str]] = list() if dbg_info else None self.update() # Export Boot Sections bs_data = bytes() # TODO: implement helper method for get key size in bytes. Now is working only with internal backend bs_offset = (ImageHeaderV2.SIZE + self.HEADER_MAC_SIZE + self.KEY_BLOB_SIZE + self.cert_block.raw_size + self.cert_block.signature_size) if self.header.flags & self.FLAGS_SHA_PRESENT_BIT: bs_offset += self.SHA_256_SIZE if not self._header.nonce: raise SPSDKError("Invalid header's nonce") counter = Counter(self._header.nonce, calc_cypher_block_count(bs_offset)) for sect in self._boot_sections: bs_data += sect.export(dek=self.dek, mac=self.mac, counter=counter, dbg_info=bs_dbg_info) # Export Header signed_data = self._header.export(padding=padding) if dbg_info: dbg_info.append("[header]") dbg_info.append(signed_data.hex()) # Add HMAC data first_bs_hmac_count = self._boot_sections[0].hmac_count hmac_data = bs_data[CmdHeader.SIZE:CmdHeader.SIZE + (first_bs_hmac_count * 32) + 32] hmac = crypto_backend().hmac(self.mac, hmac_data) signed_data += hmac if dbg_info: dbg_info.append("[hmac]") dbg_info.append(hmac.hex()) # Add KeyBlob data key_blob = crypto_backend().aes_key_wrap(self.kek, self.dek + self.mac) key_blob += b"\00" * (self.KEY_BLOB_SIZE - len(key_blob)) signed_data += key_blob if dbg_info: dbg_info.append("[key_blob]") dbg_info.append(key_blob.hex()) # Add Certificates data signed_data += self.cert_block.export() if dbg_info: dbg_info.append("[cert_block]") dbg_info.append(self.cert_block.export().hex()) # Add SHA-256 of Bootable sections if requested if self.header.flags & self.FLAGS_SHA_PRESENT_BIT: signed_data += internal_backend.hash(bs_data) # Add Signature data if not self.cert_block.verify_private_key(self.private_key_pem_data): raise SPSDKError("Private key does not match certificate") signature = crypto_backend().rsa_sign(self.private_key_pem_data, signed_data) if dbg_info: dbg_info.append("[signature]") dbg_info.append(signature.hex()) dbg_info.append("[boot_sections]") if not bs_dbg_info: raise SPSDKError("No debug information") dbg_info.extend(bs_dbg_info) return signed_data + signature + bs_data
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