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 in components: 219 if component["ComponentIdentifier"] in applicable_components_list: 220 applicable_components[components.index(component)] = 1 221 return applicable_components 222 223 224def prepare_record_descriptors(descriptors): 225 """ 226 This function processes the Descriptors and prepares the RecordDescriptors 227 section of the the firmware device ID record. 228 229 Parameters: 230 descriptors: Descriptors entry 231 232 Returns: 233 RecordDescriptors, DescriptorCount 234 """ 235 record_descriptors = bytearray() 236 vendor_defined_desc_type = 65535 237 vendor_desc_title_str_type_len = 1 238 vendor_desc_title_str_len_len = 1 239 descriptor_count = 0 240 241 for descriptor in descriptors: 242 descriptor_type = descriptor["DescriptorType"] 243 if descriptor_count == 0: 244 if ( 245 initial_descriptor_type_name_length.get(descriptor_type) 246 is None 247 ): 248 sys.exit("ERROR: Initial descriptor type not supported") 249 else: 250 if ( 251 descriptor_type_name_length.get(descriptor_type) is None 252 and descriptor_type != vendor_defined_desc_type 253 ): 254 sys.exit("ERROR: Descriptor type not supported") 255 256 if descriptor_type == vendor_defined_desc_type: 257 vendor_desc_title_str = descriptor[ 258 "VendorDefinedDescriptorTitleString" 259 ] 260 vendor_desc_data = descriptor["VendorDefinedDescriptorData"] 261 check_string_length(vendor_desc_title_str) 262 vendor_desc_title_str_type = string_types["ASCII"] 263 descriptor_length = ( 264 vendor_desc_title_str_type_len 265 + vendor_desc_title_str_len_len 266 + len(vendor_desc_title_str) 267 + len(bytearray.fromhex(vendor_desc_data)) 268 ) 269 format_string = "<HHBB" + str(len(vendor_desc_title_str)) + "s" 270 record_descriptors.extend( 271 struct.pack( 272 format_string, 273 descriptor_type, 274 descriptor_length, 275 vendor_desc_title_str_type, 276 len(vendor_desc_title_str), 277 vendor_desc_title_str.encode("ascii"), 278 ) 279 ) 280 record_descriptors.extend(bytearray.fromhex(vendor_desc_data)) 281 descriptor_count += 1 282 else: 283 descriptor_type = descriptor["DescriptorType"] 284 descriptor_data = descriptor["DescriptorData"] 285 descriptor_length = len(bytearray.fromhex(descriptor_data)) 286 if ( 287 descriptor_length 288 != descriptor_type_name_length.get(descriptor_type)[1] 289 ): 290 err_string = ( 291 "ERROR: Descriptor type - " 292 + descriptor_type_name_length.get(descriptor_type)[0] 293 + " length is incorrect" 294 ) 295 sys.exit(err_string) 296 format_string = "<HH" 297 record_descriptors.extend( 298 struct.pack(format_string, descriptor_type, descriptor_length) 299 ) 300 record_descriptors.extend(bytearray.fromhex(descriptor_data)) 301 descriptor_count += 1 302 return record_descriptors, descriptor_count 303 304 305def write_fw_device_identification_area( 306 pldm_fw_up_pkg, metadata, component_bitmap_bit_length 307): 308 """ 309 Write firmware device ID records into the PLDM package header 310 311 This function writes the DeviceIDRecordCount and the 312 FirmwareDeviceIDRecords into the firmware update package by processing the 313 metadata JSON. Currently there is no support for optional 314 FirmwareDevicePackageData. 315 316 Parameters: 317 pldm_fw_up_pkg: PLDM FW update package 318 metadata: metadata about PLDM FW update package 319 component_bitmap_bit_length: length of the ComponentBitmapBitLength 320 """ 321 # The spec limits the number of firmware device ID records to 255 322 max_device_id_record_count = 255 323 devices = metadata["FirmwareDeviceIdentificationArea"] 324 device_id_record_count = len(devices) 325 if device_id_record_count > max_device_id_record_count: 326 sys.exit( 327 "ERROR: there can be only upto 255 entries in the " 328 " FirmwareDeviceIdentificationArea section" 329 ) 330 331 # DeviceIDRecordCount 332 pldm_fw_up_pkg.write(struct.pack("<B", device_id_record_count)) 333 334 for device in devices: 335 # RecordLength size 336 record_length = 2 337 338 # DescriptorCount 339 record_length += 1 340 341 # DeviceUpdateOptionFlags 342 device_update_option_flags = bitarray(32, endian="little") 343 device_update_option_flags.setall(0) 344 # Continue component updates after failure 345 supported_device_update_option_flags = [0] 346 for option in device["DeviceUpdateOptionFlags"]: 347 if option not in supported_device_update_option_flags: 348 sys.exit("ERROR: unsupported DeviceUpdateOptionFlag entry") 349 device_update_option_flags[option] = 1 350 record_length += 4 351 352 # ComponentImageSetVersionStringType supports only ASCII for now 353 component_image_set_version_string_type = string_types["ASCII"] 354 record_length += 1 355 356 # ComponentImageSetVersionStringLength 357 component_image_set_version_string = device[ 358 "ComponentImageSetVersionString" 359 ] 360 check_string_length(component_image_set_version_string) 361 record_length += len(component_image_set_version_string) 362 record_length += 1 363 364 # Optional FirmwareDevicePackageData not supported now, 365 # FirmwareDevicePackageDataLength is set to 0x0000 366 fw_device_pkg_data_length = 0 367 record_length += 2 368 369 # ApplicableComponents 370 components = metadata["ComponentImageInformationArea"] 371 applicable_components = get_applicable_components( 372 device, components, component_bitmap_bit_length 373 ) 374 applicable_components_bitfield_length = round( 375 len(applicable_components) / 8 376 ) 377 record_length += applicable_components_bitfield_length 378 379 # RecordDescriptors 380 descriptors = device["Descriptors"] 381 record_descriptors, descriptor_count = prepare_record_descriptors( 382 descriptors 383 ) 384 record_length += len(record_descriptors) 385 386 format_string = ( 387 "<HBIBBH" 388 + str(applicable_components_bitfield_length) 389 + "s" 390 + str(len(component_image_set_version_string)) 391 + "s" 392 ) 393 pldm_fw_up_pkg.write( 394 struct.pack( 395 format_string, 396 record_length, 397 descriptor_count, 398 ba2int(device_update_option_flags), 399 component_image_set_version_string_type, 400 len(component_image_set_version_string), 401 fw_device_pkg_data_length, 402 applicable_components.tobytes(), 403 component_image_set_version_string.encode("ascii"), 404 ) 405 ) 406 pldm_fw_up_pkg.write(record_descriptors) 407 408 409def get_component_comparison_stamp(component): 410 """ 411 Get component comparison stamp from metadata file. 412 413 This function checks if ComponentOptions field is having value 1. For 414 ComponentOptions 1, ComponentComparisonStamp value from metadata file 415 is used and Default value 0xFFFFFFFF is used for other Component Options. 416 417 Parameters: 418 component: Component image info 419 Returns: 420 component_comparison_stamp: Component Comparison stamp 421 """ 422 component_comparison_stamp = 0xFFFFFFFF 423 if ( 424 int(ComponentOptions.UseComponentCompStamp) 425 in component["ComponentOptions"] 426 ): 427 # Use FD vendor selected value from metadata file 428 if "ComponentComparisonStamp" not in component.keys(): 429 sys.exit( 430 "ERROR: ComponentComparisonStamp is required" 431 " when value '1' is specified in ComponentOptions field" 432 ) 433 else: 434 try: 435 tmp_component_cmp_stmp = int( 436 component["ComponentComparisonStamp"], 16 437 ) 438 if 0 < tmp_component_cmp_stmp < 0xFFFFFFFF: 439 component_comparison_stamp = tmp_component_cmp_stmp 440 else: 441 sys.exit( 442 "ERROR: Value for ComponentComparisonStamp " 443 " should be [0x01 - 0xFFFFFFFE] when " 444 "ComponentOptions bit is set to" 445 "'1'(UseComponentComparisonStamp)" 446 ) 447 except ValueError: # invalid hext format 448 sys.exit("ERROR: Invalid hex for ComponentComparisonStamp") 449 return component_comparison_stamp 450 451 452def write_component_image_info_area(pldm_fw_up_pkg, metadata, image_files): 453 """ 454 Write component image information area into the PLDM package header 455 456 This function writes the ComponentImageCount and the 457 ComponentImageInformation into the firmware update package by processing 458 the metadata JSON. 459 460 Parameters: 461 pldm_fw_up_pkg: PLDM FW update package 462 metadata: metadata about PLDM FW update package 463 image_files: component images 464 """ 465 components = metadata["ComponentImageInformationArea"] 466 # ComponentImageCount 467 pldm_fw_up_pkg.write(struct.pack("<H", len(components))) 468 component_location_offsets = [] 469 # ComponentLocationOffset position in individual component image 470 # information 471 component_location_offset_pos = 12 472 473 for component in components: 474 # Record the location of the ComponentLocationOffset to be updated 475 # after appending images to the firmware update package 476 component_location_offsets.append( 477 pldm_fw_up_pkg.tell() + component_location_offset_pos 478 ) 479 480 # ComponentClassification 481 component_classification = component["ComponentClassification"] 482 if component_classification < 0 or component_classification > 0xFFFF: 483 sys.exit( 484 "ERROR: ComponentClassification should be [0x0000 - 0xFFFF]" 485 ) 486 487 # ComponentIdentifier 488 component_identifier = component["ComponentIdentifier"] 489 if component_identifier < 0 or component_identifier > 0xFFFF: 490 sys.exit("ERROR: ComponentIdentifier should be [0x0000 - 0xFFFF]") 491 492 # ComponentComparisonStamp 493 component_comparison_stamp = get_component_comparison_stamp(component) 494 495 # ComponentOptions 496 component_options = bitarray(16, endian="little") 497 component_options.setall(0) 498 supported_component_options = [0, 1, 2] 499 for option in component["ComponentOptions"]: 500 if option not in supported_component_options: 501 sys.exit( 502 "ERROR: unsupported ComponentOption in " 503 " ComponentImageInformationArea section" 504 ) 505 component_options[option] = 1 506 507 # RequestedComponentActivationMethod 508 requested_component_activation_method = bitarray(16, endian="little") 509 requested_component_activation_method.setall(0) 510 supported_requested_component_activation_method = [0, 1, 2, 3, 4, 5] 511 for option in component["RequestedComponentActivationMethod"]: 512 if option not in supported_requested_component_activation_method: 513 sys.exit( 514 "ERROR: unsupported RequestedComponent " 515 " ActivationMethod entry" 516 ) 517 requested_component_activation_method[option] = 1 518 519 # ComponentLocationOffset 520 component_location_offset = 0 521 # ComponentSize 522 component_size = 0 523 # ComponentVersionStringType 524 component_version_string_type = string_types["ASCII"] 525 # ComponentVersionStringlength 526 # ComponentVersionString 527 component_version_string = component["ComponentVersionString"] 528 check_string_length(component_version_string) 529 530 format_string = "<HHIHHIIBB" + str(len(component_version_string)) + "s" 531 pldm_fw_up_pkg.write( 532 struct.pack( 533 format_string, 534 component_classification, 535 component_identifier, 536 component_comparison_stamp, 537 ba2int(component_options), 538 ba2int(requested_component_activation_method), 539 component_location_offset, 540 component_size, 541 component_version_string_type, 542 len(component_version_string), 543 component_version_string.encode("ascii"), 544 ) 545 ) 546 547 index = 0 548 pkg_header_checksum_size = 4 549 start_offset = pldm_fw_up_pkg.tell() + pkg_header_checksum_size 550 # Update ComponentLocationOffset and ComponentSize for all the components 551 for offset in component_location_offsets: 552 file_size = os.stat(image_files[index]).st_size 553 pldm_fw_up_pkg.seek(offset) 554 pldm_fw_up_pkg.write(struct.pack("<II", start_offset, file_size)) 555 start_offset += file_size 556 index += 1 557 pldm_fw_up_pkg.seek(0, os.SEEK_END) 558 559 560def write_pkg_header_checksum(pldm_fw_up_pkg): 561 """ 562 Write PackageHeaderChecksum into the PLDM package header. 563 564 Parameters: 565 pldm_fw_up_pkg: PLDM FW update package 566 """ 567 pldm_fw_up_pkg.seek(0) 568 package_header_checksum = binascii.crc32(pldm_fw_up_pkg.read()) 569 pldm_fw_up_pkg.seek(0, os.SEEK_END) 570 pldm_fw_up_pkg.write(struct.pack("<I", package_header_checksum)) 571 572 573def update_pkg_header_size(pldm_fw_up_pkg): 574 """ 575 Update PackageHeader in the PLDM package header. The package header size 576 which is the count of all bytes in the PLDM package header structure is 577 calculated once the package header contents is complete. 578 579 Parameters: 580 pldm_fw_up_pkg: PLDM FW update package 581 """ 582 pkg_header_checksum_size = 4 583 file_size = pldm_fw_up_pkg.tell() + pkg_header_checksum_size 584 pkg_header_size_offset = 17 585 # Seek past PackageHeaderIdentifier and PackageHeaderFormatRevision 586 pldm_fw_up_pkg.seek(pkg_header_size_offset) 587 pldm_fw_up_pkg.write(struct.pack("<H", file_size)) 588 pldm_fw_up_pkg.seek(0, os.SEEK_END) 589 590 591def append_component_images(pldm_fw_up_pkg, image_files): 592 """ 593 Append the component images to the firmware update package. 594 595 Parameters: 596 pldm_fw_up_pkg: PLDM FW update package 597 image_files: component images 598 """ 599 for image in image_files: 600 with open(image, "rb") as file: 601 for line in file: 602 pldm_fw_up_pkg.write(line) 603 604 605def main(): 606 """Create PLDM FW update (DSP0267) package based on a JSON metadata file""" 607 parser = argparse.ArgumentParser() 608 parser.add_argument( 609 "pldmfwuppkgname", help="Name of the PLDM FW update package" 610 ) 611 parser.add_argument("metadatafile", help="Path of metadata JSON file") 612 parser.add_argument( 613 "images", 614 nargs="+", 615 help=( 616 "One or more firmware image paths, in the same order as " 617 " ComponentImageInformationArea entries" 618 ), 619 ) 620 621 args = parser.parse_args() 622 image_files = args.images 623 with open(args.metadatafile) as file: 624 try: 625 metadata = json.load(file) 626 except ValueError: 627 sys.exit("ERROR: Invalid metadata JSON file") 628 629 # Validate the number of component images 630 if len(image_files) != len(metadata["ComponentImageInformationArea"]): 631 sys.exit( 632 "ERROR: number of images passed != number of entries " 633 " in ComponentImageInformationArea" 634 ) 635 636 try: 637 with open(args.pldmfwuppkgname, "w+b") as pldm_fw_up_pkg: 638 component_bitmap_bit_length = write_pkg_header_info( 639 pldm_fw_up_pkg, metadata 640 ) 641 write_fw_device_identification_area( 642 pldm_fw_up_pkg, metadata, component_bitmap_bit_length 643 ) 644 write_component_image_info_area( 645 pldm_fw_up_pkg, metadata, image_files 646 ) 647 update_pkg_header_size(pldm_fw_up_pkg) 648 write_pkg_header_checksum(pldm_fw_up_pkg) 649 append_component_images(pldm_fw_up_pkg, image_files) 650 pldm_fw_up_pkg.close() 651 except BaseException: 652 pldm_fw_up_pkg.close() 653 os.remove(args.pldmfwuppkgname) 654 raise 655 656 657if __name__ == "__main__": 658 main() 659