gblsigntool: Import from aosp/gbl-mainline

Changes since last import:
1. aosp/3699107
2. aosp/3699126

Test: atest --host gblsigntool_test
Change-Id: Ica89c98abf343fa304303868f058dc48b6ba404f
Signed-off-by: Dmitrii Merkurev <dimorinny@google.com>
diff --git a/gbl/signtool/README.md b/gbl/signtool/README.md
index 792f0f5..b03013e 100644
--- a/gbl/signtool/README.md
+++ b/gbl/signtool/README.md
@@ -1,3 +1,9 @@
-GBL Signtool
+# GBL Signtool
 
-Commandline tool for signing GBL (Android bootloader) images.
+Commandline tool for signing GBL(Android bootloader) images.
+
+## Code Formatting
+
+Code formatting is handled using [autopep8](https://github.com/hhatto/autopep8).
+A `setup.cfg` file located in the root of the GBL project contains the relevant
+configuration settings.
diff --git a/gbl/signtool/gblsigntool.py b/gbl/signtool/gblsigntool.py
index 8ace5c5..b10595d 100644
--- a/gbl/signtool/gblsigntool.py
+++ b/gbl/signtool/gblsigntool.py
@@ -26,7 +26,7 @@
 import struct
 import tempfile
 
-from avbtool import (AvbFooter, AvbTool)
+from avbtool import AvbError, AvbFooter, AvbTool, AvbVBMetaHeader
 
 
 # Source of truth is winnt.h
@@ -56,6 +56,28 @@
 
 CERTIFICATE_TABLE_IDX = 4
 
+BLOCK_SIZE = 4096
+
+
+def align_up(num, align):
+  """Return num rounded up to the next multiple of align."""
+  return (num + align - 1) // align * align
+
+
+def get_avb_footer(buffer):
+  """Extract AVB footer from the end of the buffer."""
+  if len(buffer) >= AvbFooter.SIZE:
+    try:
+      avb_footer = AvbFooter(buffer[-AvbFooter.SIZE:])
+      vbmeta_header_start = avb_footer.vbmeta_offset
+      vbmeta_header_end = vbmeta_header_start + AvbVBMetaHeader.SIZE
+      # Check avb footer points to a valid vbmeta header.
+      AvbVBMetaHeader(buffer[vbmeta_header_start:vbmeta_header_end])
+      return avb_footer
+    except (AvbError, LookupError, struct.error):
+      pass
+  return None
+
 
 def unpack_word(buf, off):
   """Unpacks a little-endian 2-byte word."""
@@ -142,6 +164,24 @@
     return unpack_int(self._buf, OFFSET_OPTIONAL_HDR_SIZE_OF_HEADERS)
 
 
+class SectionHeaders:
+  """Helper class for PE32/PE32+ section headers."""
+
+  def __init__(self, buf, number_of_sections):
+    self._buf = buf
+    self.number_of_sections = number_of_sections
+
+  def sections_data_offsets(self):
+    """Yields (start, end) for each section."""
+    for idx in range(self.number_of_sections):
+      off = idx * 40
+      data = unpack_int(
+          self._buf, off + OFFSET_SECTION_HEADER_POINTER_TO_RAW_DATA)
+      size = unpack_int(
+          self._buf, off + OFFSET_SECTION_HEADER_SIZE_OF_RAW_DATA)
+      yield (data, data + size)
+
+
 class PEImage:
   """PE file parser."""
 
@@ -153,7 +193,7 @@
     self._optional_header_offset = (
         self._pe_header_offset + SIZEOF_IMAGE_PE_HEADER
     )
-    self._pe_header = PEHeader(self._buf[self._pe_header_offset :])
+    self._pe_header = PEHeader(self._buf[self._pe_header_offset:])
     self._checksum_offset = (
         self._optional_header_offset + OFFSET_OPTIONAL_HDR_CHECKSUM
     )
@@ -161,8 +201,15 @@
         self._optional_header_offset + self._pe_header.size_of_optional_header()
     )
     self._optional_header = OptionalHeader(
-        self._buf[self._optional_header_offset :]
+        self._buf[self._optional_header_offset:]
     )
+    self._section_headers = SectionHeaders(
+        self._buf[self._section_headers_offset:],
+        self._pe_header.number_of_sections())
+
+    # Ensure AVB footer can be found even if not at the image end
+    self.avb_footer = self._get_avb_footer_and_shrink()
+
     if self._optional_header.number_of_data_entries() < 5:
       raise PEError('PE Optional header data directories table is too small')
 
@@ -172,7 +219,7 @@
         self._optional_header_offset
         + self._optional_header.data_entry_offset(CERTIFICATE_TABLE_IDX)
     )
-    self._buf[certificate_table_offset : certificate_table_offset + 8] = (
+    self._buf[certificate_table_offset:certificate_table_offset + 8] = (
         8 * b'\x00'
     )
     if offset or size:
@@ -185,15 +232,7 @@
       print('Erased existing SecureBoot certificates')
 
   def erase_checksum(self):
-    self._buf[self._checksum_offset : self._checksum_offset + 4] = 4 * b'\x00'
-
-  def get_avb_footer(self):
-    if len(self._buf) >= AvbFooter.SIZE:
-      try:
-        return AvbFooter(self._buf[-AvbFooter.SIZE :])
-      except (LookupError, struct.error):
-        pass
-    return None
+    self._buf[self._checksum_offset:self._checksum_offset + 4] = 4 * b'\x00'
 
   def authenticode_digest(self):
     data_directory_certificate_table_offset = (
@@ -212,13 +251,7 @@
             self._optional_header.size_of_headers(),
         ),
     ]
-    for idx in range(self._pe_header.number_of_sections()):
-      off = self._section_headers_offset + idx * 40
-      size = unpack_int(self._buf, off + OFFSET_SECTION_HEADER_SIZE_OF_RAW_DATA)
-      data = unpack_int(
-          self._buf, off + OFFSET_SECTION_HEADER_POINTER_TO_RAW_DATA
-      )
-      regions.append((data, data + size))
+    regions.extend(self._section_headers.sections_data_offsets())
     regions.sort(key=lambda e: e[0])
 
     # End junk
@@ -229,6 +262,45 @@
       hasher.update(self._buf[begin:end])
     return hasher.hexdigest()
 
+  def _pe_end_offset(self):
+    """Returns the end offset of a PE file.
+
+    The end of PE file is detected as the maximum of:
+    - The end of the last section.
+    - The end of the certificate table (if present).
+    """
+    cert_offset, cert_size = self._optional_header.data_entry(
+        CERTIFICATE_TABLE_IDX)
+    certs_end_offset = cert_offset + cert_size
+    sections_end_offset = max(
+        end for _, end in self._section_headers.sections_data_offsets())
+    return max(certs_end_offset, sections_end_offset)
+
+  def _get_avb_footer_and_shrink(self):
+    """Locates an AVB footer and shrinks the buffer accordingly.
+
+    Attempts to find an AVB footer at the end of the buffer or within trailing
+    data after the PE image. If a footer is found, the buffer is shrunk to
+    handle cases where extra data was appended after flashing.
+    """
+    # Check AVB footer at the end of the image first
+    footer = get_avb_footer(self._buf)
+    if footer is not None:
+      return footer
+
+    # Avoid scanning inside the PE binary
+    pe_end_offset_aligned = align_up(self._pe_end_offset(), BLOCK_SIZE)
+    # AVB footer must be at the end of a block-aligned slice
+    for offset in range(pe_end_offset_aligned, len(self._buf) + 1, BLOCK_SIZE):
+      footer = get_avb_footer(self._buf[:offset])
+      if footer is not None:
+        footer_offset = offset - AvbFooter.SIZE
+        print(f'WARNING: AVB footer found at offset: {footer_offset}. '
+              'Expected at the end')
+        self._buf = self._buf[:offset]
+        return footer
+    return None
+
 
 def gbl_info(args):
   """Shows info about a GBL image."""
@@ -239,15 +311,14 @@
   gbl_image.erase_checksum()
   print('Authenticode digest (sha256):', gbl_image.authenticode_digest())
 
-  avb_footer = gbl_image.get_avb_footer()
-  if not avb_footer:
+  if not gbl_image.avb_footer:
     raise ValueError('No AVB footer found, image is unsigned')
 
   with tempfile.TemporaryDirectory() as temp_dir:
     gbl_efi = os.path.join(temp_dir, 'gbl.efi')
     with open(gbl_efi, 'wb') as f:
       f.write(gbl_image._buf)
-    gbl_image._buf = gbl_image._buf[: avb_footer.original_image_size]
+    gbl_image._buf = gbl_image._buf[:gbl_image.avb_footer.original_image_size]
     print(
         'Authenticode digest (without AVB footer):',
         gbl_image.authenticode_digest(),
@@ -263,9 +334,8 @@
   gbl_image = PEImage(gbl_bytes)
   gbl_image.erase_existing_win_certificates()
   gbl_image.erase_checksum()
-  avb_footer = gbl_image.get_avb_footer()
-  if avb_footer:
-    gbl_image._buf = gbl_image._buf[: avb_footer.original_image_size]
+  if gbl_image.avb_footer:
+    gbl_image._buf = gbl_image._buf[:gbl_image.avb_footer.original_image_size]
     print('Erased existing AVB footer')
   digest = gbl_image.authenticode_digest()
 
@@ -351,9 +421,8 @@
   gbl_image = PEImage(gbl_bytes)
   gbl_image.erase_existing_win_certificates()
   gbl_image.erase_checksum()
-  avb_footer = gbl_image.get_avb_footer()
-  if avb_footer:
-    gbl_image._buf = gbl_image._buf[: avb_footer.original_image_size]
+  if gbl_image.avb_footer:
+    gbl_image._buf = gbl_image._buf[:gbl_image.avb_footer.original_image_size]
     print('Erased existing AVB footer')
   with open(args.output, 'wb') as f:
     f.write(gbl_image._buf)
diff --git a/gbl/signtool/gblsigntool_test.py b/gbl/signtool/gblsigntool_test.py
index 8b00cd7..15612c5 100644
--- a/gbl/signtool/gblsigntool_test.py
+++ b/gbl/signtool/gblsigntool_test.py
@@ -14,7 +14,7 @@
 # limitations under the License.
 #
 
-
+from contextlib import contextmanager
 from pathlib import Path
 import shutil
 import subprocess
@@ -44,6 +44,10 @@
   def testInfo_success(self):
     Gblsigntool('info', TEST_EFI_PATH)
 
+  def testInfo_withTail_success(self):
+    with tmp_appended_tail(TEST_EFI_PATH) as test_efi_with_tail:
+      Gblsigntool('info', test_efi_with_tail)
+
   def testInfo_notPEImage_failure(self):
     with tempfile.TemporaryDirectory() as temp_dir:
       regular_file = Path(temp_dir).joinpath('regular_file')
@@ -74,6 +78,10 @@
   def testVerify_success(self):
     Gblsigntool('verify', TEST_EFI_PATH)
 
+  def testVerify_withTail_success(self):
+    with tmp_appended_tail(TEST_EFI_PATH) as test_efi_with_tail:
+      Gblsigntool('verify', test_efi_with_tail)
+
   def testVerify_pubkeyMismatch_failure(self):
     with self.assertRaises(subprocess.CalledProcessError):
       Gblsigntool('verify', TEST_EFI_PATH, '--key', TEST_PUBKEY_PATH)
@@ -144,5 +152,15 @@
         Gblsigntool('verify', signed_efi, '--key', TEST_PUBKEY_PATH)
 
 
+@contextmanager
+def tmp_appended_tail(path):
+  with tempfile.TemporaryDirectory() as temp_dir:
+    appended_path = Path(temp_dir) / path.name
+    shutil.copy(path, appended_path)
+    with open(appended_path, 'ab') as f:
+      f.write(b'\x00' * 4 * 4096)
+    yield appended_path
+
+
 if __name__ == '__main__':
   unittest.main(verbosity=2)