1#!/usr/bin/env python3 2 3"""Script to create PLDM FW update package""" 4 5import argparse 6import binascii 7import enum 8import json 9import math 10import os 11import struct 12import sys 13from datetime import datetime 14 15from bitarray import bitarray 16from bitarray.util import ba2int 17 18string_types = dict( 19 [ 20 ("Unknown", 0), 21 ("ASCII", 1), 22 ("UTF8", 2), 23 ("UTF16", 3), 24 ("UTF16LE", 4), 25 ("UTF16BE", 5), 26 ] 27) 28 29initial_descriptor_type_name_length = { 30 0x0000: ["PCI Vendor ID", 2], 31 0x0001: ["IANA Enterprise ID", 4], 32 0x0002: ["UUID", 16], 33 0x0003: ["PnP Vendor ID", 3], 34 0x0004: ["ACPI Vendor ID", 4], 35} 36 37descriptor_type_name_length = { 38 0x0000: ["PCI Vendor ID", 2], 39 0x0001: ["IANA Enterprise ID", 4], 40 0x0002: ["UUID", 16], 41 0x0003: ["PnP Vendor ID", 3], 42 0x0004: ["ACPI Vendor ID", 4], 43 0x0100: ["PCI Device ID", 2], 44 0x0101: ["PCI Subsystem Vendor ID", 2], 45 0x0102: ["PCI Subsystem ID", 2], 46 0x0103: ["PCI Revision ID", 1], 47 0x0104: ["PnP Product Identifier", 4], 48 0x0105: ["ACPI Product Identifier", 4], 49} 50 51 52class ComponentOptions(enum.IntEnum): 53 """ 54 Enum to represent ComponentOptions 55 """ 56 57 ForceUpdate = 0 58 UseComponentCompStamp = 1 59 60 61def check_string_length(string): 62 """Check if the length of the string is not greater than 255.""" 63 if len(string) > 255: 64 sys.exit("ERROR: Max permitted string length is 255") 65 66 67def write_pkg_release_date_time(pldm_fw_up_pkg, release_date_time): 68 """ 69 Write the timestamp into the package header. The timestamp is formatted as 70 series of 13 bytes defined in DSP0240 specification. 71 72 Parameters: 73 pldm_fw_up_pkg: PLDM FW update package 74 release_date_time: Package Release Date Time 75 """ 76 time = release_date_time.time() 77 date = release_date_time.date() 78 us_bytes = time.microsecond.to_bytes(3, byteorder="little") 79 pldm_fw_up_pkg.write( 80 struct.pack( 81 "<hBBBBBBBBHB", 82 0, 83 us_bytes[0], 84 us_bytes[1], 85 us_bytes[2], 86 time.second, 87 time.minute, 88 time.hour, 89 date.day, 90 date.month, 91 date.year, 92 0, 93 ) 94 ) 95 96 97def write_package_version_string(pldm_fw_up_pkg, metadata): 98 """ 99 Write PackageVersionStringType, PackageVersionStringLength and 100 PackageVersionString to the package header. 101 102 Parameters: 103 pldm_fw_up_pkg: PLDM FW update package 104 metadata: metadata about PLDM FW update package 105 """ 106 # Hardcoded string type to ASCII 107 string_type = string_types["ASCII"] 108 package_version_string = metadata["PackageHeaderInformation"][ 109 "PackageVersionString" 110 ] 111 check_string_length(package_version_string) 112 format_string = "<BB" + str(len(package_version_string)) + "s" 113 pldm_fw_up_pkg.write( 114 struct.pack( 115 format_string, 116 string_type, 117 len(package_version_string), 118 package_version_string.encode("ascii"), 119 ) 120 ) 121 122 123def write_component_bitmap_bit_length(pldm_fw_up_pkg, metadata): 124 """ 125 ComponentBitmapBitLength in the package header indicates the number of bits 126 that will be used represent the bitmap in the ApplicableComponents field 127 for a matching device. The value shall be a multiple of 8 and be large 128 enough to contain a bit for each component in the package. The number of 129 components in the JSON file is used to populate the bitmap length. 130 131 Parameters: 132 pldm_fw_up_pkg: PLDM FW update package 133 metadata: metadata about PLDM FW update package 134 135 Returns: 136 ComponentBitmapBitLength: number of bits that will be used 137 represent the bitmap in the ApplicableComponents field for a 138 matching device 139 """ 140 # The script supports upto 32 components now 141 max_components = 32 142 bitmap_multiple = 8 143 144 num_components = len(metadata["ComponentImageInformationArea"]) 145 if num_components > max_components: 146 sys.exit("ERROR: only upto 32 components supported now") 147 component_bitmap_bit_length = bitmap_multiple * math.ceil( 148 num_components / bitmap_multiple 149 ) 150 pldm_fw_up_pkg.write(struct.pack("<H", int(component_bitmap_bit_length))) 151 return component_bitmap_bit_length 152 153 154def write_pkg_header_info(pldm_fw_up_pkg, metadata): 155 """ 156 ComponentBitmapBitLength in the package header indicates the number of bits 157 that will be used represent the bitmap in the ApplicableComponents field 158 for a matching device. The value shall be a multiple of 8 and be large 159 enough to contain a bit for each component in the package. The number of 160 components in the JSON file is used to populate the bitmap length. 161 162 Parameters: 163 pldm_fw_up_pkg: PLDM FW update package 164 metadata: metadata about PLDM FW update package 165 166 Returns: 167 ComponentBitmapBitLength: number of bits that will be used 168 represent the bitmap in the ApplicableComponents field for a 169 matching device 170 """ 171 uuid = metadata["PackageHeaderInformation"]["PackageHeaderIdentifier"] 172 package_header_identifier = bytearray.fromhex(uuid) 173 pldm_fw_up_pkg.write(package_header_identifier) 174 175 package_header_format_revision = metadata["PackageHeaderInformation"][ 176 "PackageHeaderFormatVersion" 177 ] 178 # Size will be computed and updated subsequently 179 package_header_size = 0 180 pldm_fw_up_pkg.write( 181 struct.pack("<BH", package_header_format_revision, package_header_size) 182 ) 183 184 try: 185 release_date_time = datetime.strptime( 186 metadata["PackageHeaderInformation"]["PackageReleaseDateTime"], 187 "%d/%m/%Y %H:%M:%S", 188 ) 189 write_pkg_release_date_time(pldm_fw_up_pkg, release_date_time) 190 except KeyError: 191 write_pkg_release_date_time(pldm_fw_up_pkg, datetime.now()) 192 193 component_bitmap_bit_length = write_component_bitmap_bit_length( 194 pldm_fw_up_pkg, metadata 195 ) 196 write_package_version_string(pldm_fw_up_pkg, metadata) 197 return component_bitmap_bit_length 198 199 200def get_applicable_components(device, components, component_bitmap_bit_length): 201 """ 202 This function figures out the components applicable for the device and sets 203 the ApplicableComponents bitfield accordingly. 204 205 Parameters: 206 device: device information 207 components: list of components in the package 208 component_bitmap_bit_length: length of the ComponentBitmapBitLength 209 210 Returns: 211 The ApplicableComponents bitfield 212 """ 213 applicable_components_list = device["ApplicableComponents"] 214 applicable_components = bitarray( 215 component_bitmap_bit_length, endian="little" 216 ) 217 applicable_components.setall(0) 218 for component_index in applicable_components_list: 219 if 0 <= component_index < len(components): 220 applicable_components[component_index] = 1 221 else: 222 sys.exit("ERROR: Applicable Component index not found.") 223 return applicable_components 224 225 226def prepare_record_descriptors(descriptors): 227 """ 228 This function processes the Descriptors and prepares the RecordDescriptors 229 section of the the firmware device ID record. 230 231 Parameters: 232 descriptors: Descriptors entry 233 234 Returns: 235 RecordDescriptors, DescriptorCount 236 """ 237 record_descriptors = bytearray() 238 vendor_defined_desc_type = 65535 239 vendor_desc_title_str_type_len = 1 240 vendor_desc_title_str_len_len = 1 241 descriptor_count = 0 242 243 for descriptor in descriptors: 244 descriptor_type = descriptor["DescriptorType"] 245 if descriptor_count == 0: 246 if ( 247 initial_descriptor_type_name_length.get(descriptor_type) 248 is None 249 ): 250 sys.exit("ERROR: Initial descriptor type not supported") 251 else: 252 if ( 253 descriptor_type_name_length.get(descriptor_type) is None 254 and descriptor_type != vendor_defined_desc_type 255 ): 256 sys.exit("ERROR: Descriptor type not supported") 257 258 if descriptor_type == vendor_defined_desc_type: 259 vendor_desc_title_str = descriptor[ 260 "VendorDefinedDescriptorTitleString" 261 ] 262 vendor_desc_data = descriptor["VendorDefinedDescriptorData"] 263 check_string_length(vendor_desc_title_str) 264 vendor_desc_title_str_type = string_types["ASCII"] 265 descriptor_length = ( 266 vendor_desc_title_str_type_len 267 + vendor_desc_title_str_len_len 268 + len(vendor_desc_title_str) 269 + len(bytearray.fromhex(vendor_desc_data)) 270 ) 271 format_string = "<HHBB" + str(len(vendor_desc_title_str)) + "s" 272 record_descriptors.extend( 273 struct.pack( 274 format_string, 275 descriptor_type, 276 descriptor_length, 277 vendor_desc_title_str_type, 278 len(vendor_desc_title_str), 279 vendor_desc_title_str.encode("ascii"), 280 ) 281 ) 282 record_descriptors.extend(bytearray.fromhex(vendor_desc_data)) 283 descriptor_count += 1 284 else: 285 descriptor_type = descriptor["DescriptorType"] 286 descriptor_data = descriptor["DescriptorData"] 287 descriptor_length = len(bytearray.fromhex(descriptor_data)) 288 if ( 289 descriptor_length 290 != descriptor_type_name_length.get(descriptor_type)[1] 291 ): 292 err_string = ( 293 "ERROR: Descriptor type - " 294 + descriptor_type_name_length.get(descriptor_type)[0] 295 + " length is incorrect" 296 ) 297 sys.exit(err_string) 298 format_string = "<HH" 299 record_descriptors.extend( 300 struct.pack(format_string, descriptor_type, descriptor_length) 301 ) 302 record_descriptors.extend(bytearray.fromhex(descriptor_data)) 303 descriptor_count += 1 304 return record_descriptors, descriptor_count 305 306 307def write_fw_device_identification_area( 308 pldm_fw_up_pkg, metadata, component_bitmap_bit_length 309): 310 """ 311 Write firmware device ID records into the PLDM package header 312 313 This function writes the DeviceIDRecordCount and the 314 FirmwareDeviceIDRecords into the firmware update package by processing the 315 metadata JSON. Currently there is no support for optional 316 FirmwareDevicePackageData. 317 318 Parameters: 319 pldm_fw_up_pkg: PLDM FW update package 320 metadata: metadata about PLDM FW update package 321 component_bitmap_bit_length: length of the ComponentBitmapBitLength 322 """ 323 # The spec limits the number of firmware device ID records to 255 324 max_device_id_record_count = 255 325 devices = metadata["FirmwareDeviceIdentificationArea"] 326 device_id_record_count = len(devices) 327 if device_id_record_count > max_device_id_record_count: 328 sys.exit( 329 "ERROR: there can be only upto 255 entries in the " 330 " FirmwareDeviceIdentificationArea section" 331 ) 332 333 # DeviceIDRecordCount 334 pldm_fw_up_pkg.write(struct.pack("<B", device_id_record_count)) 335 336 for device in devices: 337 # RecordLength size 338 record_length = 2 339 340 # DescriptorCount 341 record_length += 1 342 343 # DeviceUpdateOptionFlags 344 device_update_option_flags = bitarray(32, endian="little") 345 device_update_option_flags.setall(0) 346 # Continue component updates after failure 347 supported_device_update_option_flags = [0] 348 for option in device["DeviceUpdateOptionFlags"]: 349 if option not in supported_device_update_option_flags: 350 sys.exit("ERROR: unsupported DeviceUpdateOptionFlag entry") 351 device_update_option_flags[option] = 1 352 record_length += 4 353 354 # ComponentImageSetVersionStringType supports only ASCII for now 355 component_image_set_version_string_type = string_types["ASCII"] 356 record_length += 1 357 358 # ComponentImageSetVersionStringLength 359 component_image_set_version_string = device[ 360 "ComponentImageSetVersionString" 361 ] 362 check_string_length(component_image_set_version_string) 363 record_length += len(component_image_set_version_string) 364 record_length += 1 365 366 # Optional FirmwareDevicePackageData not supported now, 367 # FirmwareDevicePackageDataLength is set to 0x0000 368 fw_device_pkg_data_length = 0 369 record_length += 2 370 371 # ApplicableComponents 372 components = metadata["ComponentImageInformationArea"] 373 applicable_components = get_applicable_components( 374 device, components, component_bitmap_bit_length 375 ) 376 applicable_components_bitfield_length = round( 377 len(applicable_components) / 8 378 ) 379 record_length += applicable_components_bitfield_length 380 381 # RecordDescriptors 382 descriptors = device["Descriptors"] 383 record_descriptors, descriptor_count = prepare_record_descriptors( 384 descriptors 385 ) 386 record_length += len(record_descriptors) 387 388 format_string = ( 389 "<HBIBBH" 390 + str(applicable_components_bitfield_length) 391 + "s" 392 + str(len(component_image_set_version_string)) 393 + "s" 394 ) 395 pldm_fw_up_pkg.write( 396 struct.pack( 397 format_string, 398 record_length, 399 descriptor_count, 400 ba2int(device_update_option_flags), 401 component_image_set_version_string_type, 402 len(component_image_set_version_string), 403 fw_device_pkg_data_length, 404 applicable_components.tobytes(), 405 component_image_set_version_string.encode("ascii"), 406 ) 407 ) 408 pldm_fw_up_pkg.write(record_descriptors) 409 410 411def get_component_comparison_stamp(component): 412 """ 413 Get component comparison stamp from metadata file. 414 415 This function checks if ComponentOptions field is having value 1. For 416 ComponentOptions 1, ComponentComparisonStamp value from metadata file 417 is used and Default value 0xFFFFFFFF is used for other Component Options. 418 419 Parameters: 420 component: Component image info 421 Returns: 422 component_comparison_stamp: Component Comparison stamp 423 """ 424 component_comparison_stamp = 0xFFFFFFFF 425 if ( 426 int(ComponentOptions.UseComponentCompStamp) 427 in component["ComponentOptions"] 428 ): 429 # Use FD vendor selected value from metadata file 430 if "ComponentComparisonStamp" not in component.keys(): 431 sys.exit( 432 "ERROR: ComponentComparisonStamp is required" 433 " when value '1' is specified in ComponentOptions field" 434 ) 435 else: 436 try: 437 tmp_component_cmp_stmp = int( 438 component["ComponentComparisonStamp"], 16 439 ) 440 if 0 < tmp_component_cmp_stmp < 0xFFFFFFFF: 441 component_comparison_stamp = tmp_component_cmp_stmp 442 else: 443 sys.exit( 444 "ERROR: Value for ComponentComparisonStamp " 445 " should be [0x01 - 0xFFFFFFFE] when " 446 "ComponentOptions bit is set to" 447 "'1'(UseComponentComparisonStamp)" 448 ) 449 except ValueError: # invalid hext format 450 sys.exit("ERROR: Invalid hex for ComponentComparisonStamp") 451 return component_comparison_stamp 452 453 454def write_component_image_info_area(pldm_fw_up_pkg, metadata, image_files): 455 """ 456 Write component image information area into the PLDM package header 457 458 This function writes the ComponentImageCount and the 459 ComponentImageInformation into the firmware update package by processing 460 the metadata JSON. 461 462 Parameters: 463 pldm_fw_up_pkg: PLDM FW update package 464 metadata: metadata about PLDM FW update package 465 image_files: component images 466 """ 467 components = metadata["ComponentImageInformationArea"] 468 # ComponentImageCount 469 pldm_fw_up_pkg.write(struct.pack("<H", len(components))) 470 component_location_offsets = [] 471 # ComponentLocationOffset position in individual component image 472 # information 473 component_location_offset_pos = 12 474 475 for component in components: 476 # Record the location of the ComponentLocationOffset to be updated 477 # after appending images to the firmware update package 478 component_location_offsets.append( 479 pldm_fw_up_pkg.tell() + component_location_offset_pos 480 ) 481 482 # ComponentClassification 483 component_classification = component["ComponentClassification"] 484 if component_classification < 0 or component_classification > 0xFFFF: 485 sys.exit( 486 "ERROR: ComponentClassification should be [0x0000 - 0xFFFF]" 487 ) 488 489 # ComponentIdentifier 490 component_identifier = component["ComponentIdentifier"] 491 if component_identifier < 0 or component_identifier > 0xFFFF: 492 sys.exit("ERROR: ComponentIdentifier should be [0x0000 - 0xFFFF]") 493 494 # ComponentComparisonStamp 495 component_comparison_stamp = get_component_comparison_stamp(component) 496 497 # ComponentOptions 498 component_options = bitarray(16, endian="little") 499 component_options.setall(0) 500 supported_component_options = [0, 1, 2] 501 for option in component["ComponentOptions"]: 502 if option not in supported_component_options: 503 sys.exit( 504 "ERROR: unsupported ComponentOption in " 505 " ComponentImageInformationArea section" 506 ) 507 component_options[option] = 1 508 509 # RequestedComponentActivationMethod 510 requested_component_activation_method = bitarray(16, endian="little") 511 requested_component_activation_method.setall(0) 512 supported_requested_component_activation_method = [0, 1, 2, 3, 4, 5] 513 for option in component["RequestedComponentActivationMethod"]: 514 if option not in supported_requested_component_activation_method: 515 sys.exit( 516 "ERROR: unsupported RequestedComponent " 517 " ActivationMethod entry" 518 ) 519 requested_component_activation_method[option] = 1 520 521 # ComponentLocationOffset 522 component_location_offset = 0 523 # ComponentSize 524 component_size = 0 525 # ComponentVersionStringType 526 component_version_string_type = string_types["ASCII"] 527 # ComponentVersionStringlength 528 # ComponentVersionString 529 component_version_string = component["ComponentVersionString"] 530 check_string_length(component_version_string) 531 532 format_string = "<HHIHHIIBB" + str(len(component_version_string)) + "s" 533 pldm_fw_up_pkg.write( 534 struct.pack( 535 format_string, 536 component_classification, 537 component_identifier, 538 component_comparison_stamp, 539 ba2int(component_options), 540 ba2int(requested_component_activation_method), 541 component_location_offset, 542 component_size, 543 component_version_string_type, 544 len(component_version_string), 545 component_version_string.encode("ascii"), 546 ) 547 ) 548 549 index = 0 550 pkg_header_checksum_size = 4 551 start_offset = pldm_fw_up_pkg.tell() + pkg_header_checksum_size 552 # Update ComponentLocationOffset and ComponentSize for all the components 553 for offset in component_location_offsets: 554 file_size = os.stat(image_files[index]).st_size 555 pldm_fw_up_pkg.seek(offset) 556 pldm_fw_up_pkg.write(struct.pack("<II", start_offset, file_size)) 557 start_offset += file_size 558 index += 1 559 pldm_fw_up_pkg.seek(0, os.SEEK_END) 560 561 562def write_pkg_header_checksum(pldm_fw_up_pkg): 563 """ 564 Write PackageHeaderChecksum into the PLDM package header. 565 566 Parameters: 567 pldm_fw_up_pkg: PLDM FW update package 568 """ 569 pldm_fw_up_pkg.seek(0) 570 package_header_checksum = binascii.crc32(pldm_fw_up_pkg.read()) 571 pldm_fw_up_pkg.seek(0, os.SEEK_END) 572 pldm_fw_up_pkg.write(struct.pack("<I", package_header_checksum)) 573 574 575def update_pkg_header_size(pldm_fw_up_pkg): 576 """ 577 Update PackageHeader in the PLDM package header. The package header size 578 which is the count of all bytes in the PLDM package header structure is 579 calculated once the package header contents is complete. 580 581 Parameters: 582 pldm_fw_up_pkg: PLDM FW update package 583 """ 584 pkg_header_checksum_size = 4 585 file_size = pldm_fw_up_pkg.tell() + pkg_header_checksum_size 586 pkg_header_size_offset = 17 587 # Seek past PackageHeaderIdentifier and PackageHeaderFormatRevision 588 pldm_fw_up_pkg.seek(pkg_header_size_offset) 589 pldm_fw_up_pkg.write(struct.pack("<H", file_size)) 590 pldm_fw_up_pkg.seek(0, os.SEEK_END) 591 592 593def append_component_images(pldm_fw_up_pkg, image_files): 594 """ 595 Append the component images to the firmware update package. 596 597 Parameters: 598 pldm_fw_up_pkg: PLDM FW update package 599 image_files: component images 600 """ 601 for image in image_files: 602 with open(image, "rb") as file: 603 for line in file: 604 pldm_fw_up_pkg.write(line) 605 606 607def main(): 608 """Create PLDM FW update (DSP0267) package based on a JSON metadata file""" 609 parser = argparse.ArgumentParser() 610 parser.add_argument( 611 "pldmfwuppkgname", help="Name of the PLDM FW update package" 612 ) 613 parser.add_argument("metadatafile", help="Path of metadata JSON file") 614 parser.add_argument( 615 "images", 616 nargs="+", 617 help=( 618 "One or more firmware image paths, in the same order as " 619 " ComponentImageInformationArea entries" 620 ), 621 ) 622 623 args = parser.parse_args() 624 image_files = args.images 625 with open(args.metadatafile) as file: 626 try: 627 metadata = json.load(file) 628 except ValueError: 629 sys.exit("ERROR: Invalid metadata JSON file") 630 631 # Validate the number of component images 632 if len(image_files) != len(metadata["ComponentImageInformationArea"]): 633 sys.exit( 634 "ERROR: number of images passed != number of entries " 635 " in ComponentImageInformationArea" 636 ) 637 638 try: 639 with open(args.pldmfwuppkgname, "w+b") as pldm_fw_up_pkg: 640 component_bitmap_bit_length = write_pkg_header_info( 641 pldm_fw_up_pkg, metadata 642 ) 643 write_fw_device_identification_area( 644 pldm_fw_up_pkg, metadata, component_bitmap_bit_length 645 ) 646 write_component_image_info_area( 647 pldm_fw_up_pkg, metadata, image_files 648 ) 649 update_pkg_header_size(pldm_fw_up_pkg) 650 write_pkg_header_checksum(pldm_fw_up_pkg) 651 append_component_images(pldm_fw_up_pkg, image_files) 652 pldm_fw_up_pkg.close() 653 except BaseException: 654 pldm_fw_up_pkg.close() 655 os.remove(args.pldmfwuppkgname) 656 raise 657 658 659if __name__ == "__main__": 660 main() 661