signing: add errors and add/update signers

 * Add GBBSigner
 * updated BiosSigner use of keys, version and loemdir
 * Rewrite ECSigner to do signing in tempdir
 * Add Key and Signer errors
 * Add ec key for KeysetFromDir()
 * Add subdir arg to KeysetFromSigner

BUG=chromium:813829
TEST=signing/*_unittest

Change-Id: Ifee5e537024e47be14a9687df689ad07be9df612
Reviewed-on: https://chromium-review.googlesource.com/1211122
Commit-Ready: Chris Ching <chingcodes@chromium.org>
Tested-by: Chris Ching <chingcodes@chromium.org>
Reviewed-by: Lann Martin <lannm@chromium.org>
diff --git a/signing/lib/firmware.py b/signing/lib/firmware.py
index d5bb353..7626270 100644
--- a/signing/lib/firmware.py
+++ b/signing/lib/firmware.py
@@ -8,77 +8,143 @@
 from __future__ import print_function
 
 import os
+import re
 import shutil
 import tempfile
 
 from chromite.lib import cros_build_lib
 from chromite.lib import cros_logging as logging
 from chromite.lib import osutils
-from chromite.signing.lib.signer import FutilitySigner
+from chromite.signing.lib import signer
 
 
-class BiosSigner(FutilitySigner):
+class BiosSigner(signer.FutilitySigner):
   """Sign bios.bin file using futility."""
 
   _required_keys_private = ('firmware_data_key',)
-  _required_keys_public = ('kernel',)
+  _required_keys_public = ('kernel_subkey',)
   _required_keyblocks = ('firmware_data_key',)
 
-  def __init__(self, bios_version=1, sig_dir=None, sig_id=None):
+  def __init__(self, sig_id='', sig_dir=''):
     """Init BiosSigner
 
     Args:
-      bios_version: Version of the bios being signed
-      sig_dir: Signature directory (aka loem dir)
       sig_id: Signature ID (aka loem id)
+      sig_dir: Signature Output Directory (i.e shellball/keyset)
     """
-    self.version = bios_version
-    self.sig_dir = sig_dir
     self.sig_id = sig_id
+    self.sig_dir = sig_dir
 
   def GetFutilityArgs(self, keyset, input_name, output_name):
+    """Returns futility arguments for signing bios
+
+    Args:
+      keyset: keyset used for signing
+      input_name: bios image
+      output_name: output firmware file
+    """
     fw_key = keyset.keys['firmware_data_key']
-
-
-    kernel_key = keyset.keys['kernel']
+    kernel_key = keyset.keys['kernel_subkey']
+    dev_fw_key = keyset.keys.get('dev_firmware_data_key', fw_key)
 
     args = ['sign',
             '--type', 'bios',
             '--signprivate', fw_key.private,
             '--keyblock', fw_key.keyblock,
             '--kernelkey', kernel_key.public,
-            '--version', str(self.version)]
-
-    # Add developer key arguments
-    dev_fw_key = keyset.keys.get('dev_firmware_data_key')
-
-    if dev_fw_key is not None:
-      args += ['--devsign', dev_fw_key.private,
-               '--devkeyblock', dev_fw_key.keyblock]
-    else:
-      # Fallback to fw_key if device key not found (legacy, still needed?)
-      args += ['--devsign', fw_key.private,
-               '--devkeyblock', fw_key.keyblock]
+            '--version', fw_key.version,
+            '--devsign', dev_fw_key.private,
+            '--devkeyblock', dev_fw_key.keyblock]
 
     # Add loem related arguments
-    if self.sig_dir is not None and self.sig_id is not None:
+    if self.sig_id and self.sig_dir:
       args += ['--loemdir', self.sig_dir,
                '--loemid', self.sig_id]
 
     # Add final input/output arguments
     args += [input_name, output_name]
+
     return args
 
 
-class ECSigner(FutilitySigner):
+class ECSigner(signer.BaseSigner):
   """Sign EC bin file."""
 
-  _required_keys_private = ('ec',)
+  _required_keys_private = ('key_ec_efs',)
+
+  def IsROSigned(self, firmware_image):
+    """Returns True if the given firmware has RO signed ec."""
+
+    # Check fmap for KEY_RO
+    fmap = cros_build_lib.RunCommand(['futility', 'dump_fmap', firmware_image],
+                                     capture_output=True)
+
+    return re.search('KEY_RO', fmap.output) is not None
+
+  def Sign(self, keyset, input_name, output_name):
+    """"Sign EC image
+
+    Args:
+      keyset: keyset used for this signing step
+      input_name: ec image path to be signed (i.e. to ec.bin)
+      output_name: bios image path to be updated with new hashes
+    """
+    # Use absolute paths since we use a temp directory
+    ec_path = os.path.abspath(input_name)
+    bios_path = os.path.abspath(output_name)
+
+    if self.IsROSigned(bios_path):
+      # Only sign if not read-only, nothing to do
+      return True
+
+    logging.info('Signing EC %s', ec_path)
+
+    # Run futility in temp_dir to avoid cwd artifacts
+    with osutils.TempDir() as temp_dir:
+      ec_rw_bin = os.path.join(temp_dir, 'EC_RW.bin')
+      ec_rw_hash = os.path.join(temp_dir, 'EC_RW.hash')
+      try:
+        cros_build_lib.RunCommand(['futility', 'sign',
+                                   '--type', 'rwsig',
+                                   '--prikey',
+                                   keyset.keys['key_ec_efs'].private,
+                                   ec_path],
+                                  cwd=temp_dir)
+
+        cros_build_lib.RunCommand(['openssl', 'dgst', '-sha256', '-binary',
+                                   ec_rw_bin],
+                                  log_stdout_to_file=ec_rw_hash,
+                                  cwd=temp_dir)
+
+        cros_build_lib.RunCommand(['store_file_in_cbfs', bios_path,
+                                   ec_rw_bin, 'ecrw'])
+
+        cros_build_lib.RunCommand(['store_file_in_cbfs', bios_path,
+                                   ec_rw_hash, 'ecrw.hash'])
+
+      except cros_build_lib.RunCommandError as err:
+        logging.warning('Signing EC failed: %s', str(err))
+        return False
+
+    return True
+
+
+class GBBSigner(signer.FutilitySigner):
+  """Sign GBB"""
+  _required_keys_public = ('recovery_key',)
+  _required_keys_private = ('root_key',)
 
   def GetFutilityArgs(self, keyset, input_name, output_name):
-    return ['sign',
-            '--type', 'rwsig',
-            '--prikey', keyset.keys['ec'].private,
+    """Return args for signing GBB
+
+    Args:
+      keyset: Keyset used for signing
+      input_name: Firmware image
+      output_name: Bios path (i.e. tobios.bin)
+    """
+    return ['gbb',
+            '--set',
+            '--recoverykey=' + keyset.keys['recovery_key'].public,
             input_name,
             output_name]
 
diff --git a/signing/lib/firmware_unittest.py b/signing/lib/firmware_unittest.py
index 5c1e209..ea5736d 100644
--- a/signing/lib/firmware_unittest.py
+++ b/signing/lib/firmware_unittest.py
@@ -17,90 +17,136 @@
 from chromite.signing.lib import signer_unittest
 
 
-class TestBiosSigner(cros_test_lib.RunCommandTestCase,
-                     cros_test_lib.TempDirTestCase):
+class TestBiosSigner(cros_test_lib.RunCommandTempDirTestCase):
   """Test BiosSigner."""
 
   def testGetCmdArgs(self):
     bs = firmware.BiosSigner()
     ks = signer_unittest.KeysetFromSigner(bs, self.tempdir)
 
-    fw_key = ks.keys['firmware_data_key']
+    bios_bin = os.path.join(self.tempdir, 'bios.bin')
+    bios_out = os.path.join(self.tempdir, 'bios.out')
 
-    self.assertListEqual(bs.GetFutilityArgs(ks, 'foo', 'bar'),
+    fw_key = ks.keys['firmware_data_key']
+    kernel_key = ks.keys['kernel_subkey']
+    self.assertListEqual(bs.GetFutilityArgs(ks, bios_bin, bios_out),
                          ['sign',
                           '--type', 'bios',
                           '--signprivate', fw_key.private,
                           '--keyblock', fw_key.keyblock,
-                          '--kernelkey', ks.keys['kernel'].public,
-                          '--version', str(bs.version),
+                          '--kernelkey', kernel_key.public,
+                          '--version', fw_key.version,
                           '--devsign', fw_key.private,
                           '--devkeyblock', fw_key.keyblock,
-                          'foo', 'bar'])
+                          bios_bin, bios_out])
 
   def testGetCmdArgsWithDevKeys(self):
     bs = firmware.BiosSigner()
     ks = signer_unittest.KeysetFromSigner(bs, self.tempdir)
 
+    bios_bin = os.path.join(self.tempdir, 'bios.bin')
+    bios_out = os.path.join(self.tempdir, 'bios.out')
+
     # Add 'dev_firmware' keys and keyblock
     dev_fw_key = keys.KeyPair('dev_firmware_data_key', keydir=self.tempdir)
     ks.AddKey(dev_fw_key)
     keys_unittest.CreateDummyPrivateKey(dev_fw_key)
-
     keys_unittest.CreateDummyKeyblock(dev_fw_key)
 
     fw_key = ks.keys['firmware_data_key']
+    kernel_key = ks.keys['kernel_subkey']
 
-    self.assertListEqual(bs.GetFutilityArgs(ks, 'foo', 'bar'),
+    self.assertListEqual(bs.GetFutilityArgs(ks, bios_bin, bios_out),
                          ['sign',
                           '--type', 'bios',
                           '--signprivate', fw_key.private,
                           '--keyblock', fw_key.keyblock,
-                          '--kernelkey', ks.keys['kernel'].public,
-                          '--version', str(bs.version),
+                          '--kernelkey', kernel_key.public,
+                          '--version', fw_key.version,
                           '--devsign', dev_fw_key.private,
                           '--devkeyblock', dev_fw_key.keyblock,
-                          'foo', 'bar'])
+                          bios_bin, bios_out])
 
   def testGetCmdArgsWithSig(self):
-    loem_dir = os.path.join(self.tempdir, 'loem')
+    loem_dir = os.path.join(self.tempdir, 'loem1', 'keyset')
     loem_id = 'loem1'
-    bs = firmware.BiosSigner(sig_dir=loem_dir, sig_id=loem_id)
-    ks = signer_unittest.KeysetFromSigner(bs, self.tempdir)
+
+    bs = firmware.BiosSigner(sig_id=loem_id, sig_dir=loem_dir)
+    ks = signer_unittest.KeysetFromSigner(bs, keydir=self.tempdir)
+
+    bios_bin = os.path.join(self.tempdir, loem_id, 'bios.bin')
 
     fw_key = ks.keys['firmware_data_key']
+    kernel_key = ks.keys['kernel_subkey']
 
-    self.assertListEqual(bs.GetFutilityArgs(ks, 'foo', 'bar'),
+    self.assertListEqual(bs.GetFutilityArgs(ks, bios_bin, loem_dir),
                          ['sign',
                           '--type', 'bios',
                           '--signprivate', fw_key.private,
                           '--keyblock', fw_key.keyblock,
-                          '--kernelkey', ks.keys['kernel'].public,
-                          '--version', str(bs.version),
+                          '--kernelkey', kernel_key.public,
+                          '--version', fw_key.version,
                           '--devsign', fw_key.private,
                           '--devkeyblock', fw_key.keyblock,
                           '--loemdir', loem_dir,
                           '--loemid', loem_id,
-                          'foo', 'bar'])
+                          bios_bin, loem_dir])
 
 
-class TestECSigner(cros_test_lib.RunCommandTestCase,
-                   cros_test_lib.TempDirTestCase):
+class TestECSigner(cros_test_lib.RunCommandTempDirTestCase):
   """Test ECSigner."""
 
-  def testGetCmdArgs(self):
-    ecs = firmware.ECSigner()
-    ks = signer_unittest.KeysetFromSigner(ecs, self.tempdir)
+  def testIsROSignedRW(self):
+    ec_signer = firmware.ECSigner()
+    bios_bin = os.path.join(self.tempdir, 'bin.bin')
 
-    self.assertListEqual(ecs.GetFutilityArgs(ks, 'foo', 'bar'),
-                         ['sign',
-                          '--type', 'rwsig',
-                          '--prikey', ks.keys['ec'].private,
-                          'foo', 'bar'])
+    self.assertFalse(ec_signer.IsROSigned(bios_bin))
+
+    self.assertCommandContains(['futility', 'dump_fmap', bios_bin])
+
+  def testIsROSignedRO(self):
+    ec_signer = firmware.ECSigner()
+    bios_bin = os.path.join(self.tempdir, 'bin.bin')
+
+    self.rc.SetDefaultCmdResult(output='KEY_RO')
+
+    self.assertTrue(ec_signer.IsROSigned(bios_bin))
+
+  def testSign(self):
+    ec_signer = firmware.ECSigner()
+    ks = signer_unittest.KeysetFromSigner(ec_signer, self.tempdir)
+    ec_bin = os.path.join(self.tempdir, 'ec.bin')
+    bios_bin = os.path.join(self.tempdir, 'bios.bin')
+
+    self.assertTrue(ec_signer.Sign(ks, ec_bin, bios_bin))
+
+    self.assertCommandContains(['futility', 'sign', '--type', 'rwsig',
+                                '--prikey', ks.keys['key_ec_efs'].private,
+                                ec_bin])
+    self.assertCommandContains(['openssl', 'dgst', '-sha256', '-binary'])
+    self.assertCommandContains(['store_file_in_cbfs', bios_bin, 'ecrw'])
+    self.assertCommandContains(['store_file_in_cbfs', bios_bin, 'ecrw.hash'])
 
 
-class ShellballTest(cros_test_lib.RunCommandTestCase,
-                    cros_test_lib.TempDirTestCase):
+class TestGBBSigner(cros_test_lib.TempDirTestCase):
+  """Test GBBSigner."""
+
+  def testGetFutilityArgs(self):
+    gb_signer = firmware.GBBSigner()
+    ks = signer_unittest.KeysetFromSigner(gb_signer, self.tempdir)
+
+    bios_in = os.path.join(self.tempdir, 'bin.orig')
+    bios_out = os.path.join(self.tempdir, 'bios.bin')
+
+    self.assertListEqual(gb_signer.GetFutilityArgs(ks, bios_in, bios_out),
+                         ['gbb',
+                          '--set',
+                          '--recoverykey=' + ks.keys['recovery_key'].public,
+                          bios_in,
+                          bios_out])
+
+
+class ShellballTest(cros_test_lib.RunCommandTempDirTestCase):
   """Verify that shellball is being called with correct arguments."""
 
   def setUp(self):
diff --git a/signing/lib/keys.py b/signing/lib/keys.py
index 2a0c848..e4c304c 100644
--- a/signing/lib/keys.py
+++ b/signing/lib/keys.py
@@ -14,6 +14,18 @@
 from chromite.lib import cros_logging as logging
 
 
+class SignerKeyError(Exception):
+  """Raise when there in an error related to keys"""
+
+
+class SignerKeyMissingError(SignerKeyError):
+  """Raise if key is missing from subset"""
+
+
+class SignerSubkeyMissingError(SignerKeyMissingError):
+  """Raise if a subkey is missing"""
+
+
 class KeyPair(object):
   """Container for a key's files.
 
@@ -157,19 +169,28 @@
     * Keys does not have a subkey of the given name
     * Subkey exists for the given name, or alias. Key will be indexed under
         it's parent key's name. ex: 'firmware.loem1' -> 'firmware'
+
+    Raises SubkeyMissingError if subkey not found
     """
     ks = Keyset()
 
+    found = False
+
     # Use alias if exists
     subkey_alias = self.subkey_aliases.get(subkey_name, '')
 
     for key_name, key in self.keys.items():
       if subkey_alias in key.subkeys:
+        found = True
         ks.AddKey(key.subkeys[subkey_alias], key_name=key_name)
       elif subkey_name in key.subkeys:
+        found = True
         ks.AddKey(key.subkeys[subkey_name], key_name=key_name)
       else:
-        ks.AddKey(key)
+        ks.AddKey(key, key_name=key_name)
+
+    if not found:
+      raise SignerSubkeyMissingError("Unable to find %s", subkey_name)
 
     return ks
 
@@ -216,4 +237,8 @@
         logging.debug('Found subkey %s.%s', key_name, subkey)
         ks.AddSubkey(key_name, subkey)
 
+    elif f_name == 'key_ec_efs.vbprik2':
+      ks.AddKey(KeyPair('key_ec_efs', key_dir,
+                        pub_ext='.vbpubk2', priv_ext='.vbprivk2'))
+
   return ks
diff --git a/signing/lib/keys_unittest.py b/signing/lib/keys_unittest.py
index 251d7f9..59d898f 100644
--- a/signing/lib/keys_unittest.py
+++ b/signing/lib/keys_unittest.py
@@ -21,6 +21,7 @@
           'firmware_data_key',
           'installer_kernel_data_key',
           'kernel_data_key',
+          'kernel_subkey'
           'recovery_kernel_data_key',
           'root_key')
 
@@ -193,7 +194,7 @@
   def testEqSame(self):
     kc1 = self._get_keyset()
     kc2 = self._get_keyset()
-    self.assertTrue(kc1 == kc2)
+    self.assertEqual(kc1, kc2)
 
   def testEqDiffrent(self):
     kc1 = self._get_keyset()
@@ -231,10 +232,9 @@
 
   def testGetSubKeysetMissmatch(self):
     ks0 = self._get_keyset()
-    ks1 = ks0.GetSubKeyset('foo')
 
-    self.assertIsInstance(ks1, keys.Keyset)
-    self.assertDictEqual(ks0.keys, ks1.keys)
+    with self.assertRaises(keys.SignerSubkeyMissingError):
+      ks0.GetSubKeyset('foo')
 
   def testGetSubKeyset(self):
     ks0 = self._get_keyset()
@@ -341,7 +341,7 @@
     for subkey in key.subkeys.values():
       CreateDummyPublic(subkey)
   else:
-    osutils.Touch(key.public)
+    osutils.Touch(key.public, makedirs=True)
 
 
 def CreateDummyPrivateKey(key):
@@ -350,7 +350,7 @@
     for subkey in key.subkeys.values():
       CreateDummyPrivateKey(subkey)
   else:
-    osutils.Touch(key.private)
+    osutils.Touch(key.private, makedirs=True)
 
 
 def CreateDummyKeyblock(key):
@@ -359,7 +359,7 @@
     for subkey in key.subkeys.values():
       CreateDummyKeyblock(subkey)
   else:
-    osutils.Touch(key.keyblock)
+    osutils.Touch(key.keyblock, makedirs=True)
 
 
 def CreateDummyKeys(key):
diff --git a/signing/lib/signer.py b/signing/lib/signer.py
index 1bda934..3fd9d1b 100644
--- a/signing/lib/signer.py
+++ b/signing/lib/signer.py
@@ -34,6 +34,9 @@
 from chromite.lib import cros_build_lib
 
 
+class SigningFailedError(Exception):
+  """Raise when a signing failed"""
+
 class SignerOutputTemplateError(Exception):
   """Raise when there is an issue with filling a signer output template"""
 
diff --git a/signing/lib/signer_unittest.py b/signing/lib/signer_unittest.py
index 08957cd..3363534 100644
--- a/signing/lib/signer_unittest.py
+++ b/signing/lib/signer_unittest.py
@@ -9,6 +9,7 @@
 
 import ConfigParser
 import io
+import os
 
 from chromite.lib import cros_test_lib
 from chromite.signing.lib import keys
@@ -194,10 +195,12 @@
     self.assertTrue(s0.CheckKeyset(ks0))
 
 
-def KeysetFromSigner(s, keydir):
+def KeysetFromSigner(s, keydir, subdir='keyset'):
   """Returns a valid keyset containing required keys and keyblocks."""
   ks = keys.Keyset()
 
+  keydir = os.path.join(keydir, subdir)
+
   # pylint: disable=protected-access
   for key_name in s._required_keys:
     key = keys.KeyPair(key_name, keydir=keydir)
@@ -236,8 +239,7 @@
     return [input_name, output_name]
 
 
-class TestFutilitySigner(cros_test_lib.RunCommandTestCase,
-                         cros_test_lib.TempDirTestCase):
+class TestFutilitySigner(cros_test_lib.RunCommandTempDirTestCase):
   """Test Futility Signer."""
 
   def testSign(self):