cros payload: --signatures flag shows the metadata signatures

The existing --signatures flag now dumps the metadata signatures
embedded in the payload.

BUG=None
TEST=Added unittests; manually check a signed payload.

Change-Id: I4e2eb5cafeabbafd40af31716840d405a79b1023
Reviewed-on: https://chromium-review.googlesource.com/310654
Commit-Ready: Alex Deymo <deymo@chromium.org>
Tested-by: Alex Deymo <deymo@chromium.org>
Reviewed-by: Sen Jiang <senj@chromium.org>
diff --git a/cli/cros/cros_payload.py b/cli/cros/cros_payload.py
index 61c4141..9581b59 100644
--- a/cli/cros/cros_payload.py
+++ b/cli/cros/cros_payload.py
@@ -8,7 +8,6 @@
 
 import itertools
 import os
-import string
 import sys
 import textwrap
 
@@ -37,7 +36,7 @@
           ' '.join('%.2x' % ord(c) for c in chunk) +
           '   ' * (16 - len(chunk)) +
           ' | ' +
-          ''.join(c if c in string.printable else '.' for c in chunk))
+          ''.join(c if 32 <= ord(c) and ord(c) < 127 else '.' for c in chunk))
 
 
 @command.CommandDecorator('payload')
@@ -101,22 +100,38 @@
 
   def _DisplaySignatures(self):
     """Show information about the signatures from the manifest."""
+    header = self.payload.header
+    if header.metadata_signature_len:
+      offset = header.size + header.manifest_len
+      DisplayValue('Metadata signatures blob',
+                   'file_offset=%d (%d bytes)' %
+                   (offset, header.metadata_signature_len))
+      signatures_blob = self.payload.ReadDataBlob(
+          -header.metadata_signature_len,
+          header.metadata_signature_len)
+      self._DisplaySignaturesBlob('Metadata', signatures_blob)
+    else:
+      print('No metadata signatures stored in the payload')
+
     manifest = self.payload.manifest
-    if not manifest.HasField('signatures_offset'):
-      print('No signatures stored in the payload')
-      return
+    if manifest.HasField('signatures_offset'):
+      signature_msg = 'blob_offset=%d' % manifest.signatures_offset
+      if manifest.signatures_size:
+        signature_msg += ' (%d bytes)' % manifest.signatures_size
+      DisplayValue('Payload signatures blob', signature_msg)
+      signatures_blob = self.payload.ReadDataBlob(manifest.signatures_offset,
+                                                  manifest.signatures_size)
+      self._DisplaySignaturesBlob('Payload', signatures_blob)
+    else:
+      print('No payload signatures stored in the payload')
 
-    signature_msg = 'offset=%d' % manifest.signatures_offset
-    if manifest.signatures_size:
-      signature_msg += ' (%d bytes)' % manifest.signatures_size
-    DisplayValue('Signature blob', signature_msg)
-    signatures_blob = self.payload.ReadDataBlob(manifest.signatures_offset,
-                                                manifest.signatures_size)
-
+  @staticmethod
+  def _DisplaySignaturesBlob(signature_name, signatures_blob):
     from dev.host.lib.update_payload import update_metadata_pb2
     signatures = update_metadata_pb2.Signatures()
     signatures.ParseFromString(signatures_blob)
-    print('Payload signatures: (%d entries)' % len(signatures.signatures))
+    print('%s signatures: (%d entries)' %
+          (signature_name, len(signatures.signatures)))
     for signature in signatures.signatures:
       print('  version=%s, hex_data: (%d bytes)' %
             (signature.version if signature.HasField('version') else None,
diff --git a/cli/cros/cros_payload_unittest.py b/cli/cros/cros_payload_unittest.py
index f8fd78d..36f4749 100644
--- a/cli/cros/cros_payload_unittest.py
+++ b/cli/cros/cros_payload_unittest.py
@@ -93,19 +93,32 @@
     """Fake HasField method based on the python members."""
     return hasattr(self, field_name) and getattr(self, field_name) is not None
 
+class FakeHeader(object):
+  """Fake payload header for testing."""
+
+  def __init__(self, version, manifest_len, metadata_signature_len):
+    self.version = version
+    self.manifest_len = manifest_len
+    self.metadata_signature_len = metadata_signature_len
+
+  @property
+  def size(self):
+    return (20 if self.version == cros_payload.MAJOR_PAYLOAD_VERSION_CHROMEOS
+            else 24)
+
+
 class FakePayload(object):
   """Fake payload for testing."""
 
   def __init__(self, major_version):
-    FakeHeader = collections.namedtuple('FakeHeader',
-                                        ['version', 'manifest_len'])
-    self._header = FakeHeader(major_version, 222)
+    self._header = FakeHeader(major_version, 222, 0)
     self.header = None
     self._manifest = FakeManifest(major_version)
     self.manifest = None
 
     self._blobs = {}
-    self._signatures = update_metadata_pb2.Signatures()
+    self._payload_signatures = update_metadata_pb2.Signatures()
+    self._metadata_signatures = update_metadata_pb2.Signatures()
 
   def Init(self):
     """Fake Init that sets header and manifest.
@@ -126,15 +139,28 @@
                              'actual: %d)' % (len(blob), length))
     return blob
 
-  def AddSignature(self, **kwargs):
-    new_signature = self._signatures.signatures.add()
+  @staticmethod
+  def _AddSignatureToProto(proto, **kwargs):
+    """Add a new Signature element to the passed proto."""
+    new_signature = proto.signatures.add()
     for key, val in kwargs.iteritems():
       setattr(new_signature, key, val)
-    blob = self._signatures.SerializeToString()
+
+  def AddPayloadSignature(self, **kwargs):
+    self._AddSignatureToProto(self._payload_signatures, **kwargs)
+    blob = self._payload_signatures.SerializeToString()
     self._manifest.signatures_offset = 1234
     self._manifest.signatures_size = len(blob)
     self._blobs[self._manifest.signatures_offset] = blob
 
+  def AddMetadataSignature(self, **kwargs):
+    self._AddSignatureToProto(self._metadata_signatures, **kwargs)
+    if self._header.metadata_signature_len:
+      del self._blobs[-self._header.metadata_signature_len]
+    blob = self._metadata_signatures.SerializeToString()
+    self._header.metadata_signature_len = len(blob)
+    self._blobs[-len(blob)] = blob
+
 
 class PayloadCommandTest(cros_test_lib.MockOutputTestCase):
   """Test class for our PayloadCommand class."""
@@ -298,7 +324,8 @@
 Number of kernel ops:    1
 Block size:              4096
 Minor version:           4
-No signatures stored in the payload
+No metadata signatures stored in the payload
+No payload signatures stored in the payload
 """
     self.assertEquals(stdout, expected_out)
 
@@ -307,22 +334,29 @@
     """Verify that the --signatures option shows the present signatures."""
     payload_cmd = cros_payload.PayloadCommand(
         FakeOption(action='show', signatures=True))
-    payload = FakePayload(cros_payload.MAJOR_PAYLOAD_VERSION_CHROMEOS)
-    payload.AddSignature(version=1, data='12345678abcdefgh\x00\x01\x02\x03')
-    payload.AddSignature(data='I am a signature so access is yes.')
+    payload = FakePayload(cros_payload.MAJOR_PAYLOAD_VERSION_BRILLO)
+    payload.AddPayloadSignature(version=1,
+                                data='12345678abcdefgh\x00\x01\x02\x03')
+    payload.AddPayloadSignature(data='I am a signature so access is yes.')
+    payload.AddMetadataSignature(data='\x00\x0a\x0c')
     self.PatchObject(update_payload, 'Payload', return_value=payload)
 
     with self.OutputCapturer() as output:
       payload_cmd.Run()
 
     stdout = output.GetStdout()
-    expected_out = """Payload version:         1
+    expected_out = """Payload version:         2
 Manifest length:         222
-Number of operations:    1
-Number of kernel ops:    1
+Number of partitions:    2
+  Number of "rootfs" ops: 1
+  Number of "kernel" ops: 1
 Block size:              4096
 Minor version:           4
-Signature blob:          offset=1234 (64 bytes)
+Metadata signatures blob: file_offset=246 (7 bytes)
+Metadata signatures: (1 entries)
+  version=None, hex_data: (3 bytes)
+    00 0a 0c                                        | ...
+Payload signatures blob: blob_offset=1234 (64 bytes)
 Payload signatures: (2 entries)
   version=1, hex_data: (20 bytes)
     31 32 33 34 35 36 37 38 61 62 63 64 65 66 67 68 | 12345678abcdefgh