releasetools: Add PayloadSigner class.

Create a wrapper class that handles the payload signing, which unifies
the paths with and without external signer. Also add tests for the newly
added class.

The test keys under testdata/ are created with the script in
development/tools/make_key. testdata/testkey_with_passwd.pk8 uses
password "foo".

Test: python -m unittest test_ota_from_target_files
Test: Get identical A/B OTA packages w/ and w/o the CL.
Change-Id: Ic770aec726498a3babb88ec509985e7f1210fb18
diff --git a/tools/releasetools/ota_from_target_files.py b/tools/releasetools/ota_from_target_files.py
index 88cb741..95b7303 100755
--- a/tools/releasetools/ota_from_target_files.py
+++ b/tools/releasetools/ota_from_target_files.py
@@ -310,6 +310,56 @@
       script.AssertOemProperty(prop, values, oem_no_mount)
 
 
+class PayloadSigner(object):
+  """A class that wraps the payload signing works.
+
+  When generating a Payload, hashes of the payload and metadata files will be
+  signed with the device key, either by calling an external payload signer or
+  by calling openssl with the package key. This class provides a unified
+  interface, so that callers can just call PayloadSigner.Sign().
+
+  If an external payload signer has been specified (OPTIONS.payload_signer), it
+  calls the signer with the provided args (OPTIONS.payload_signer_args). Note
+  that the signing key should be provided as part of the payload_signer_args.
+  Otherwise without an external signer, it uses the package key
+  (OPTIONS.package_key) and calls openssl for the signing works.
+  """
+
+  def __init__(self):
+    if OPTIONS.payload_signer is None:
+      # Prepare the payload signing key.
+      private_key = OPTIONS.package_key + OPTIONS.private_key_suffix
+      pw = OPTIONS.key_passwords[OPTIONS.package_key]
+
+      cmd = ["openssl", "pkcs8", "-in", private_key, "-inform", "DER"]
+      cmd.extend(["-passin", "pass:" + pw] if pw else ["-nocrypt"])
+      signing_key = common.MakeTempFile(prefix="key-", suffix=".key")
+      cmd.extend(["-out", signing_key])
+
+      get_signing_key = common.Run(cmd, verbose=False, stdout=subprocess.PIPE,
+                                   stderr=subprocess.STDOUT)
+      stdoutdata, _ = get_signing_key.communicate()
+      assert get_signing_key.returncode == 0, \
+          "Failed to get signing key: {}".format(stdoutdata)
+
+      self.signer = "openssl"
+      self.signer_args = ["pkeyutl", "-sign", "-inkey", signing_key,
+                          "-pkeyopt", "digest:sha256"]
+    else:
+      self.signer = OPTIONS.payload_signer
+      self.signer_args = OPTIONS.payload_signer_args
+
+  def Sign(self, in_file):
+    """Signs the given input file. Returns the output filename."""
+    out_file = common.MakeTempFile(prefix="signed-", suffix=".bin")
+    cmd = [self.signer] + self.signer_args + ['-in', in_file, '-out', out_file]
+    signing = common.Run(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
+    stdoutdata, _ = signing.communicate()
+    assert signing.returncode == 0, \
+        "Failed to sign the input file: {}".format(stdoutdata)
+    return out_file
+
+
 def SignOutput(temp_zip_name, output_zip_name):
   pw = OPTIONS.key_passwords[OPTIONS.package_key]
 
@@ -1076,20 +1126,8 @@
   # The place where the output from the subprocess should go.
   log_file = sys.stdout if OPTIONS.verbose else subprocess.PIPE
 
-  # A/B updater expects a signing key in RSA format. Gets the key ready for
-  # later use in step 3, unless a payload_signer has been specified.
-  if OPTIONS.payload_signer is None:
-    cmd = ["openssl", "pkcs8",
-           "-in", OPTIONS.package_key + OPTIONS.private_key_suffix,
-           "-inform", "DER"]
-    pw = OPTIONS.key_passwords[OPTIONS.package_key]
-    cmd.extend(["-passin", "pass:" + pw] if pw else ["-nocrypt"])
-    rsa_key = common.MakeTempFile(prefix="key-", suffix=".key")
-    cmd.extend(["-out", rsa_key])
-    p1 = common.Run(cmd, verbose=False, stdout=log_file,
-                    stderr=subprocess.STDOUT)
-    p1.communicate()
-    assert p1.returncode == 0, "openssl pkcs8 failed"
+  # Get the PayloadSigner to be used in step 3.
+  payload_signer = PayloadSigner()
 
   # Stage the output zip package for package signing.
   temp_zip_file = tempfile.NamedTemporaryFile()
@@ -1130,37 +1168,11 @@
   assert p1.returncode == 0, "brillo_update_payload hash failed"
 
   # 3. Sign the hashes and insert them back into the payload file.
-  signed_payload_sig_file = common.MakeTempFile(prefix="signed-sig-",
-                                                suffix=".bin")
-  signed_metadata_sig_file = common.MakeTempFile(prefix="signed-sig-",
-                                                 suffix=".bin")
   # 3a. Sign the payload hash.
-  if OPTIONS.payload_signer is not None:
-    cmd = [OPTIONS.payload_signer]
-    cmd.extend(OPTIONS.payload_signer_args)
-  else:
-    cmd = ["openssl", "pkeyutl", "-sign",
-           "-inkey", rsa_key,
-           "-pkeyopt", "digest:sha256"]
-  cmd.extend(["-in", payload_sig_file,
-              "-out", signed_payload_sig_file])
-  p1 = common.Run(cmd, stdout=log_file, stderr=subprocess.STDOUT)
-  p1.communicate()
-  assert p1.returncode == 0, "openssl sign payload failed"
+  signed_payload_sig_file = payload_signer.Sign(payload_sig_file)
 
   # 3b. Sign the metadata hash.
-  if OPTIONS.payload_signer is not None:
-    cmd = [OPTIONS.payload_signer]
-    cmd.extend(OPTIONS.payload_signer_args)
-  else:
-    cmd = ["openssl", "pkeyutl", "-sign",
-           "-inkey", rsa_key,
-           "-pkeyopt", "digest:sha256"]
-  cmd.extend(["-in", metadata_sig_file,
-              "-out", signed_metadata_sig_file])
-  p1 = common.Run(cmd, stdout=log_file, stderr=subprocess.STDOUT)
-  p1.communicate()
-  assert p1.returncode == 0, "openssl sign metadata failed"
+  signed_metadata_sig_file = payload_signer.Sign(metadata_sig_file)
 
   # 3c. Insert the signatures back into the payload file.
   signed_payload_file = common.MakeTempFile(prefix="signed-payload-",
diff --git a/tools/releasetools/test_ota_from_target_files.py b/tools/releasetools/test_ota_from_target_files.py
index 5f6c5d0..fa6655b 100644
--- a/tools/releasetools/test_ota_from_target_files.py
+++ b/tools/releasetools/test_ota_from_target_files.py
@@ -15,11 +15,20 @@
 #
 
 import copy
+import os.path
 import unittest
 
 import common
 from ota_from_target_files import (
-    _LoadOemDicts, BuildInfo, GetPackageMetadata, WriteFingerprintAssertion)
+    _LoadOemDicts, BuildInfo, GetPackageMetadata, PayloadSigner,
+    WriteFingerprintAssertion)
+
+
+def get_testdata_dir():
+  """Returns the testdata dir, in relative to the script dir."""
+  # The script dir is the one we want, which could be different from pwd.
+  current_dir = os.path.dirname(os.path.realpath(__file__))
+  return os.path.join(current_dir, 'testdata')
 
 
 class MockScriptWriter(object):
@@ -476,3 +485,82 @@
             'pre-build-incremental' : 'build-version-incremental-source',
         },
         metadata)
+
+
+class PayloadSignerTest(unittest.TestCase):
+
+  SIGFILE = 'sigfile.bin'
+  SIGNED_SIGFILE = 'signed-sigfile.bin'
+
+  def setUp(self):
+    self.testdata_dir = get_testdata_dir()
+    self.assertTrue(os.path.exists(self.testdata_dir))
+
+    common.OPTIONS.payload_signer = None
+    common.OPTIONS.payload_signer_args = []
+    common.OPTIONS.package_key = os.path.join(self.testdata_dir, 'testkey')
+    common.OPTIONS.key_passwords = {
+        common.OPTIONS.package_key : None,
+    }
+
+  def tearDown(self):
+    common.Cleanup()
+
+  def _assertFilesEqual(self, file1, file2):
+    with open(file1, 'rb') as fp1, open(file2, 'rb') as fp2:
+      self.assertEqual(fp1.read(), fp2.read())
+
+  def test_init(self):
+    payload_signer = PayloadSigner()
+    self.assertEqual('openssl', payload_signer.signer)
+
+  def test_init_withPassword(self):
+    common.OPTIONS.package_key = os.path.join(
+        self.testdata_dir, 'testkey_with_passwd')
+    common.OPTIONS.key_passwords = {
+        common.OPTIONS.package_key : 'foo',
+    }
+    payload_signer = PayloadSigner()
+    self.assertEqual('openssl', payload_signer.signer)
+
+  def test_init_withExternalSigner(self):
+    common.OPTIONS.payload_signer = 'abc'
+    common.OPTIONS.payload_signer_args = ['arg1', 'arg2']
+    payload_signer = PayloadSigner()
+    self.assertEqual('abc', payload_signer.signer)
+    self.assertEqual(['arg1', 'arg2'], payload_signer.signer_args)
+
+  def test_Sign(self):
+    payload_signer = PayloadSigner()
+    input_file = os.path.join(self.testdata_dir, self.SIGFILE)
+    signed_file = payload_signer.Sign(input_file)
+
+    verify_file = os.path.join(self.testdata_dir, self.SIGNED_SIGFILE)
+    self._assertFilesEqual(verify_file, signed_file)
+
+  def test_Sign_withExternalSigner_openssl(self):
+    """Uses openssl as the external payload signer."""
+    common.OPTIONS.payload_signer = 'openssl'
+    common.OPTIONS.payload_signer_args = [
+        'pkeyutl', '-sign', '-keyform', 'DER', '-inkey',
+        os.path.join(self.testdata_dir, 'testkey.pk8'),
+        '-pkeyopt', 'digest:sha256']
+    payload_signer = PayloadSigner()
+    input_file = os.path.join(self.testdata_dir, self.SIGFILE)
+    signed_file = payload_signer.Sign(input_file)
+
+    verify_file = os.path.join(self.testdata_dir, self.SIGNED_SIGFILE)
+    self._assertFilesEqual(verify_file, signed_file)
+
+  def test_Sign_withExternalSigner_script(self):
+    """Uses testdata/payload_signer.sh as the external payload signer."""
+    common.OPTIONS.payload_signer = os.path.join(
+        self.testdata_dir, 'payload_signer.sh')
+    common.OPTIONS.payload_signer_args = [
+        os.path.join(self.testdata_dir, 'testkey.pk8')]
+    payload_signer = PayloadSigner()
+    input_file = os.path.join(self.testdata_dir, self.SIGFILE)
+    signed_file = payload_signer.Sign(input_file)
+
+    verify_file = os.path.join(self.testdata_dir, self.SIGNED_SIGFILE)
+    self._assertFilesEqual(verify_file, signed_file)
diff --git a/tools/releasetools/testdata/payload_signer.sh b/tools/releasetools/testdata/payload_signer.sh
new file mode 100755
index 0000000..a44ef34
--- /dev/null
+++ b/tools/releasetools/testdata/payload_signer.sh
@@ -0,0 +1,4 @@
+#!/bin/sh
+
+# The script will be called with 'payload_signer.sh <key> -in <input> -out <output>'.
+openssl pkeyutl -sign -keyform DER -inkey $1 -pkeyopt digest:sha256 -in $3 -out $5
diff --git a/tools/releasetools/testdata/sigfile.bin b/tools/releasetools/testdata/sigfile.bin
new file mode 100644
index 0000000..8682216
--- /dev/null
+++ b/tools/releasetools/testdata/sigfile.bin
@@ -0,0 +1 @@
+ºQàÂÜ¢”¡½¨Gpø£Õùù°ÔÖ'[4KéL¡c
\ No newline at end of file
diff --git a/tools/releasetools/testdata/signed-sigfile.bin b/tools/releasetools/testdata/signed-sigfile.bin
new file mode 100644
index 0000000..86d2f9e
--- /dev/null
+++ b/tools/releasetools/testdata/signed-sigfile.bin
@@ -0,0 +1,2 @@
+R¡&‹EÿsÁ%ø?¹|¤œ&Í€ñzbSŠA[ßtqç†WKґl¦àÙÙås¥Ò~Fcæ	`ž¯¾Í#
+T{Ý×Û½F­ÒÁŸxƒø1‰6̋=Q°•ŒVæ^Tß°ØxX£¶/þ#©êI'ÜîtcLp““¬­ŸëovzђRá:õóWþ9(¹Á26Û̬ábÂBP1¸6ãnÒß±QÕC©gh;r‰²O}%Ľõˆáo6ã”d“ê´Éãå2Y`¦ÕÛ¼ª¥_R“OrCa,èI"n(`–ínñÜÐbaiö¹Å¨ÔäS„×Ê)kžO[`6c¬e
\ No newline at end of file
diff --git a/tools/releasetools/testdata/testkey.pk8 b/tools/releasetools/testdata/testkey.pk8
new file mode 100644
index 0000000..99be291
--- /dev/null
+++ b/tools/releasetools/testdata/testkey.pk8
Binary files differ
diff --git a/tools/releasetools/testdata/testkey.x509.pem b/tools/releasetools/testdata/testkey.x509.pem
new file mode 100644
index 0000000..65c8085
--- /dev/null
+++ b/tools/releasetools/testdata/testkey.x509.pem
@@ -0,0 +1,24 @@
+-----BEGIN CERTIFICATE-----
+MIIEADCCAuigAwIBAgIJAN/FvjYzGNOKMA0GCSqGSIb3DQEBCwUAMIGUMQswCQYD
+VQQGEwJVUzETMBEGA1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNTW91bnRhaW4g
+VmlldzEQMA4GA1UECgwHQW5kcm9pZDEQMA4GA1UECwwHQW5kcm9pZDEQMA4GA1UE
+AwwHQW5kcm9pZDEiMCAGCSqGSIb3DQEJARYTYW5kcm9pZEBhbmRyb2lkLmNvbTAe
+Fw0xODAxMTgwMDM0NTFaFw00NTA2MDUwMDM0NTFaMIGUMQswCQYDVQQGEwJVUzET
+MBEGA1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNTW91bnRhaW4gVmlldzEQMA4G
+A1UECgwHQW5kcm9pZDEQMA4GA1UECwwHQW5kcm9pZDEQMA4GA1UEAwwHQW5kcm9p
+ZDEiMCAGCSqGSIb3DQEJARYTYW5kcm9pZEBhbmRyb2lkLmNvbTCCASIwDQYJKoZI
+hvcNAQEBBQADggEPADCCAQoCggEBAL478jti8FoJkDcqu8/sStOHoNLdwC+MtjYa
+QADs1ZxcggKxXBYy0xkAw75G2T+jddjuvncCaDy57Z5vQPlZzyBRUR4NB1FkmxzP
+kJPCYL9v9gFZAFI+Sda/beF/tliNHkcyT9eWY5+vKUChpnMnIq8tIG75mL1y9mVJ
+k5ueg5hHwlAkSGNiBifwnDJxXiLVVNC8SrFeTJbeQTtFb/wleBGoji8Mgp6GblIW
+LaO3R5Tv+O7/x/c4ZCQueDgNXZA9/BD4DuRp34RhUjV0EZiQ016xYHejvkDuMlDV
+/JWD9dDM4plKSLWWtObevDQA6sGJd0+51s77gva+CKmQ8j39tU0CAwEAAaNTMFEw
+HQYDVR0OBBYEFNJPJZDpq6tc/19Z2kxPA2bj9D6UMB8GA1UdIwQYMBaAFNJPJZDp
+q6tc/19Z2kxPA2bj9D6UMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQAD
+ggEBABSUG9qrwV3WcClDJwqkNLN4yeVVYzkRMGA8/XqOiYrW4zh0mKDLfr6OeU1C
+AKwZBLhhql59Po25r4gcwPiTN2DkoCfb3T59XG8J54PAgTQjIAZ3J+mGZplnmuD3
+wj+UGUpPe0qTr33ZPoJfwxVo4RVnOt/UCsIGXch0HS/BIdpechqP0w4rOHUbq6EA
+8UEi5irKSDOU9b/5rD/tX2f4nGwJlKQEHWrsj9LLKlaL7fX36ghoSxN/pBJOhedg
+/VjT6xbaEwfyhC6Zj9av5Xl7UdpYt+rBMroAGenz0OSxKhIphdcx4ZMhvfkBoYG9
+Crupdqe+kUsfg2RlPb5grQ3klMo=
+-----END CERTIFICATE-----
diff --git a/tools/releasetools/testdata/testkey_with_passwd.pk8 b/tools/releasetools/testdata/testkey_with_passwd.pk8
new file mode 100644
index 0000000..3d567de
--- /dev/null
+++ b/tools/releasetools/testdata/testkey_with_passwd.pk8
Binary files differ
diff --git a/tools/releasetools/testdata/testkey_with_passwd.x509.pem b/tools/releasetools/testdata/testkey_with_passwd.x509.pem
new file mode 100644
index 0000000..449396e
--- /dev/null
+++ b/tools/releasetools/testdata/testkey_with_passwd.x509.pem
@@ -0,0 +1,24 @@
+-----BEGIN CERTIFICATE-----
+MIIEADCCAuigAwIBAgIJANefUd3Piu0yMA0GCSqGSIb3DQEBCwUAMIGUMQswCQYD
+VQQGEwJVUzETMBEGA1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNTW91bnRhaW4g
+VmlldzEQMA4GA1UECgwHQW5kcm9pZDEQMA4GA1UECwwHQW5kcm9pZDEQMA4GA1UE
+AwwHQW5kcm9pZDEiMCAGCSqGSIb3DQEJARYTYW5kcm9pZEBhbmRyb2lkLmNvbTAe
+Fw0xODAxMTgwMDI3NDRaFw00NTA2MDUwMDI3NDRaMIGUMQswCQYDVQQGEwJVUzET
+MBEGA1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNTW91bnRhaW4gVmlldzEQMA4G
+A1UECgwHQW5kcm9pZDEQMA4GA1UECwwHQW5kcm9pZDEQMA4GA1UEAwwHQW5kcm9p
+ZDEiMCAGCSqGSIb3DQEJARYTYW5kcm9pZEBhbmRyb2lkLmNvbTCCASIwDQYJKoZI
+hvcNAQEBBQADggEPADCCAQoCggEBALBoA4c+qCQKapQAVGclbousC5J/L0TNZJEd
+KSW2nzXUHIwgTQ3r82227xkIvjnqXMCsc0q3/N2gGKR4sHqA30JO9Dyfgsx1ISaR
+GXe5cG048m5U5snplQgvPovtah9ZyvwNPzWPYC3uceJaDxKQKwVdsV+mOWM6WmpQ
+bdLO37jxfytyAbzaz3sG5HA3FSB8rX/xDM6If18NsxSHpcjaOjZXC4Fg6wlp0klY
+5/qhFEdmieu2zQVelXjoJfKSku8tPa7kZeDU/F3uLUq/U/xvFk7NVsRV+QvYOdQK
+1QECc/3yv1TKNAN3huWTgzCX6bMHmi09Npw3MQaGY0oS34cH9x0CAwEAAaNTMFEw
+HQYDVR0OBBYEFNsJZ0n9Opeea0rVAzL+1jwkDKzPMB8GA1UdIwQYMBaAFNsJZ0n9
+Opeea0rVAzL+1jwkDKzPMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQAD
+ggEBAJ/bzIzA+NrYwPEv56XKf6Vuj81+M1rTHAsH9PqbOvJT7iM7aU7wAl6vmXAo
+DQtvKoOBMdIXprapwe0quHCQm7PGxg+RRegr+dcTSVJFv1plnODOBOEAVlEfFwuW
+Cz0USF2jrNq+4ciH5zPL1a31ONb1rMkxJXQ/tAi0x8m6tZz+jsbE0wO6qB80UmkA
+4WY2Tu/gnAvFpD8plkiU0EKwedBHAcaFFZkQp23MKsVZ3UBqsqzzfXDYV1Oa6rIy
+XIZpI2Gx75pvAb57T2ap/yl0DBEAu7Nmpll0GCsgeJVdy7tS4LNj96Quya3CHWQw
+WNTVuan0KZqwDIm4Xn1oHUFQ9vc=
+-----END CERTIFICATE-----