image_signing: unit tests for Android image

- move helper functions that detect which keys should be used depending
on the build flavor to a separate lib
- add unit tests for that lib

BUG=b:72947583
TEST=unit tests
TEST=run against caroline image, scripts detects 'cheets' build flavor
TEST=run against novato-arc64 image (SDK), script detects 'cheets' build
flavor
TEST=run against newbie image (AOSP), script detects 'aosp' build flavor
TEST=run against invalid build property 'paosp_cheets_...', script
aborts as expected
BRANCH=None

Change-Id: I5595c10a5a063e7658d0cf17c77dbeead429cd97
Reviewed-on: https://chromium-review.googlesource.com/923097
Commit-Ready: Nicolas Norvez <norvez@chromium.org>
Tested-by: Nicolas Norvez <norvez@chromium.org>
Reviewed-by: Mike Frysinger <vapier@chromium.org>
diff --git a/Makefile b/Makefile
index 78af5a6..1f3e7f6 100644
--- a/Makefile
+++ b/Makefile
@@ -1392,6 +1392,7 @@
 
 .PHONY: runtestscripts
 runtestscripts: test_setup genfuzztestcases
+	scripts/image_signing/sign_android_unittests.sh
 	tests/load_kernel_tests.sh
 	tests/run_cgpt_tests.sh ${BUILD_RUN}/cgpt/cgpt
 	tests/run_cgpt_tests.sh ${BUILD_RUN}/cgpt/cgpt -D 358400
diff --git a/scripts/image_signing/lib/sign_android_lib.sh b/scripts/image_signing/lib/sign_android_lib.sh
new file mode 100644
index 0000000..eae2a36
--- /dev/null
+++ b/scripts/image_signing/lib/sign_android_lib.sh
@@ -0,0 +1,133 @@
+#!/bin/bash
+
+# Copyright 2018 The Chromium OS Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+. "$(dirname "$0")/common.sh"
+
+#######################################
+# Return name according to the current signing debug key. The name is used to
+# select key files.
+# Globals:
+#   None
+# Arguments:
+#   sha1: signature of the APK.
+#   keyset: "cheets" or "aosp" build?
+# Outputs:
+#   Writes the name of the key to stdout.
+# Returns:
+#   0 on success, non-zero on error.
+#######################################
+android_choose_key() {
+  local sha1="$1"
+  local keyset="$2"
+
+  if [[ "${keyset}" != "aosp" && "${keyset}" != "cheets" ]]; then
+    error "Unknown Android build keyset '${keyset}'."
+    return 1
+  fi
+
+  # Fingerprints below are generated by:
+  # 'cheets' keyset:
+  # $ keytool -file vendor/google/certs/cheetskeys/$NAME.x509.pem -printcert \
+  #     | grep SHA1:
+  # 'aosp' keyset:
+  # $ keytool -file build/target/product/security/$NAME.x509.pem -printcert \
+  #     | grep SHA1:
+  declare -A platform_sha=(
+    ['cheets']='AA:04:E0:5F:82:9C:7E:D1:B9:F8:FC:99:6C:5A:54:43:83:D9:F5:BC'
+    ['aosp']='27:19:6E:38:6B:87:5E:76:AD:F7:00:E7:EA:84:E4:C6:EE:E3:3D:FA'
+  )
+  declare -A media_sha=(
+    ['cheets']='D4:C4:2D:E0:B9:1B:15:72:FA:7D:A7:21:E0:A6:09:94:B4:4C:B5:AE'
+    ['aosp']='B7:9D:F4:A8:2E:90:B5:7E:A7:65:25:AB:70:37:AB:23:8A:42:F5:D3'
+  )
+  declare -A shared_sha=(
+    ['cheets']='38:B6:2C:E1:75:98:E3:E1:1C:CC:F6:6B:83:BB:97:0E:2D:40:6C:AE'
+    ['aosp']='5B:36:8C:FF:2D:A2:68:69:96:BC:95:EA:C1:90:EA:A4:F5:63:0F:E5'
+  )
+  declare -A release_sha=(
+    ['cheets']='EC:63:36:20:23:B7:CB:66:18:70:D3:39:3C:A9:AE:7E:EF:A9:32:42'
+    ['aosp']='61:ED:37:7E:85:D3:86:A8:DF:EE:6B:86:4B:D8:5B:0B:FA:A5:AF:81'
+  )
+
+  case "${sha1}" in
+    "${platform_sha["${keyset}"]}")
+      echo "platform"
+      ;;
+    "${media_sha["${keyset}"]}")
+      echo "media"
+      ;;
+    "${shared_sha["${keyset}"]}")
+      echo "shared"
+      ;;
+    "${release_sha["${keyset}"]}")
+      # The release_sha[] fingerprint is from devkey. Translate to releasekey.
+      echo "releasekey"
+      ;;
+    *)
+      # Not a framework apk.  Do not re-sign.
+      echo ""
+      ;;
+  esac
+  return 0
+}
+
+#######################################
+# Extract 'ro.build.flavor' property from build property file.
+# Globals:
+#   None
+# Arguments:
+#   build_prop_file: path to build property file.
+# Outputs:
+#   Writes the value of the property to stdout.
+# Returns:
+#   0 on success, non-zero on error.
+#######################################
+android_get_build_flavor_prop() {
+  local build_prop_file="$1"
+  local flavor_prop=""
+
+  if ! flavor_prop=$(grep -a "^ro\.build\.flavor=" "${build_prop_file}"); then
+    return 1
+  fi
+  flavor_prop=$(echo "${flavor_prop}" | cut -d "=" -f2)
+  echo "${flavor_prop}"
+  return 0
+}
+
+#######################################
+# Pick the expected keyset ('cheets', 'aosp') depending on the build flavor.
+# Globals:
+#   None
+# Arguments:
+#   flavor_prop: the value of the build flavor property.
+# Outputs:
+#   Writes the name of the keyset to stdout.
+# Returns:
+#   0 on success, non-zero on error.
+#######################################
+android_choose_signing_keyset() {
+  local flavor_prop="$1"
+
+  # Property ro.build.flavor follows those patterns:
+  # - cheets builds:
+  #   ro.build.flavor=cheets_${arch}-user(debug)
+  # - SDK builds:
+  #   ro.build.flavor=sdk_google_cheets_${arch}-user(debug)
+  # - AOSP builds:
+  #   ro.build.flavor=aosp_cheets_${arch}-user(debug)
+  # "cheets" and "SDK" builds both use the same signing keys, cheetskeys. "AOSP"
+  # builds use the public AOSP signing keys.
+  if [[ "${flavor_prop}" == aosp_cheets_* ]]; then
+    keyset="aosp"
+  elif [[ "${flavor_prop}" == cheets_* ||
+    "${flavor_prop}" == sdk_google_cheets_* ]]; then
+    keyset="cheets"
+  else
+    return 1
+  fi
+  echo "${keyset}"
+  return 0
+}
diff --git a/scripts/image_signing/sign_android_image.sh b/scripts/image_signing/sign_android_image.sh
index a205b5a..5a6d321 100755
--- a/scripts/image_signing/sign_android_image.sh
+++ b/scripts/image_signing/sign_android_image.sh
@@ -5,6 +5,7 @@
 # found in the LICENSE file.
 
 . "$(dirname "$0")/common.sh"
+. "$(dirname "$0")/lib/sign_android_lib.sh"
 
 set -e
 
@@ -31,63 +32,6 @@
   exit 0
 }
 
-# Return name according to the current signing debug key. The name is used to
-# select key files.
-choose_key() {
-  local sha1="$1"
-  local keyset="$2"
-
-  if [[ "${keyset}" != "aosp" && "${keyset}" != "cheets" ]]; then
-    error "Unknown Android build keyset '${keyset}'"
-    return 1
-  fi
-
-  # Fingerprints below are generated by:
-  # 'cheets' keyset:
-  # $ keytool -file vendor/google/certs/cheetskeys/$NAME.x509.pem -printcert \
-  #     | grep SHA1:
-  # 'aosp' keyset:
-  # $ keytool -file build/target/product/security/$NAME.x509.pem -printcert \
-  #     | grep SHA1:
-  declare -A platform_sha=(
-    ['cheets']='AA:04:E0:5F:82:9C:7E:D1:B9:F8:FC:99:6C:5A:54:43:83:D9:F5:BC'
-    ['aosp']='27:19:6E:38:6B:87:5E:76:AD:F7:00:E7:EA:84:E4:C6:EE:E3:3D:FA'
-  )
-  declare -A media_sha=(
-    ['cheets']='D4:C4:2D:E0:B9:1B:15:72:FA:7D:A7:21:E0:A6:09:94:B4:4C:B5:AE'
-    ['aosp']='B7:9D:F4:A8:2E:90:B5:7E:A7:65:25:AB:70:37:AB:23:8A:42:F5:D3'
-  )
-  declare -A shared_sha=(
-    ['cheets']='38:B6:2C:E1:75:98:E3:E1:1C:CC:F6:6B:83:BB:97:0E:2D:40:6C:AE'
-    ['aosp']='5B:36:8C:FF:2D:A2:68:69:96:BC:95:EA:C1:90:EA:A4:F5:63:0F:E5'
-  )
-  declare -A release_sha=(
-    ['cheets']='EC:63:36:20:23:B7:CB:66:18:70:D3:39:3C:A9:AE:7E:EF:A9:32:42'
-    ['aosp']='61:ED:37:7E:85:D3:86:A8:DF:EE:6B:86:4B:D8:5B:0B:FA:A5:AF:81'
-  )
-
-  case "${sha1}" in
-    "${platform_sha["${keyset}"]}")
-      echo "platform"
-      ;;
-    "${media_sha["${keyset}"]}")
-      echo "media"
-      ;;
-    "${shared_sha["${keyset}"]}")
-      echo "shared"
-      ;;
-    "${release_sha["${keyset}"]}")
-      # The release_sha[] fingerprint is from devkey. Translate to releasekey.
-      echo "releasekey"
-      ;;
-    *)
-      # Not a framework apk.  Do not re-sign.
-      echo ""
-      ;;
-  esac
-  return 0
-}
-
 # Re-sign framework apks with the corresponding release keys.  Only apk with
 # known key fingerprint are re-signed.  We should not re-sign non-framework
 # apks.
@@ -97,25 +41,13 @@
   local flavor_prop=""
   local keyset=""
 
-  # Property ro.build.flavor follows those patterns:
-  # - cheets builds:
-  #   ro.build.flavor=cheets_${arch}-user(debug)
-  # - SDK builds:
-  #   ro.build.flavor=sdk_google_cheets_${arch}-user(debug)
-  # - AOSP builds:
-  #   ro.build.flavor=aosp_cheets_${arch}-user(debug)
-  # "cheets" and "SDK" builds both use the same signing keys, cheetskeys. "AOSP"
-  # builds use the public AOSP signing keys.
-  flavor_prop=$(grep -a "^ro\.build\.flavor=" \
-    "${system_mnt}/system/build.prop" | cut -d "=" -f2)
-
+  if ! flavor_prop=$(android_get_build_flavor_prop \
+    "${system_mnt}/system/build.prop"); then
+    die "Failed to extract build flavor property from \
+'${system_mnt}/system/build.prop'."
+  fi
   info "Found build flavor property '${flavor_prop}'."
-  if [[ "${flavor_prop}" == aosp_cheets_* ]]; then
-    keyset="aosp"
-  elif [[ "${flavor_prop}" == cheets_* ||
-    "${flavor_prop}" == sdk_google_cheets_* ]]; then
-    keyset="cheets"
-  else
+  if ! keyset=$(android_choose_signing_keyset "${flavor_prop}"); then
     die "Unknown build flavor property '${flavor_prop}'."
   fi
   info "Expecting signing keyset '${keyset}'."
@@ -137,7 +69,7 @@
     sha1=$(unzip -p "${apk}" META-INF/CERT.RSA | \
       keytool -printcert | awk '/^\s*SHA1:/ {print $2}')
 
-    if  ! keyname=$(choose_key "${sha1}" "${keyset}"); then
+    if  ! keyname=$(android_choose_key "${sha1}" "${keyset}"); then
       die "Failed to choose signing key for APK '${apk}' (SHA1 '${sha1}') in \
 build flavor '${flavor_prop}'."
     fi
diff --git a/scripts/image_signing/sign_android_unittests.sh b/scripts/image_signing/sign_android_unittests.sh
new file mode 100755
index 0000000..00a651f
--- /dev/null
+++ b/scripts/image_signing/sign_android_unittests.sh
@@ -0,0 +1,210 @@
+#!/bin/bash
+
+# Copyright 2018 The Chromium OS Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+. "$(dirname "$0")/lib/sign_android_lib.sh"
+
+# Expected APK signatures depending on the type of APK and the type of build.
+declare -A platform_sha=(
+  ['cheets']='AA:04:E0:5F:82:9C:7E:D1:B9:F8:FC:99:6C:5A:54:43:83:D9:F5:BC'
+  ['aosp']='27:19:6E:38:6B:87:5E:76:AD:F7:00:E7:EA:84:E4:C6:EE:E3:3D:FA'
+)
+declare -A media_sha=(
+  ['cheets']='D4:C4:2D:E0:B9:1B:15:72:FA:7D:A7:21:E0:A6:09:94:B4:4C:B5:AE'
+  ['aosp']='B7:9D:F4:A8:2E:90:B5:7E:A7:65:25:AB:70:37:AB:23:8A:42:F5:D3'
+)
+declare -A shared_sha=(
+  ['cheets']='38:B6:2C:E1:75:98:E3:E1:1C:CC:F6:6B:83:BB:97:0E:2D:40:6C:AE'
+  ['aosp']='5B:36:8C:FF:2D:A2:68:69:96:BC:95:EA:C1:90:EA:A4:F5:63:0F:E5'
+)
+declare -A release_sha=(
+  ['cheets']='EC:63:36:20:23:B7:CB:66:18:70:D3:39:3C:A9:AE:7E:EF:A9:32:42'
+  ['aosp']='61:ED:37:7E:85:D3:86:A8:DF:EE:6B:86:4B:D8:5B:0B:FA:A5:AF:81'
+)
+
+
+test_android_choose_key_invalid_keyset() {
+  local keyname
+  local keyset
+  local keysets=("invalid_keyset" " " "")
+
+  for keyset in "${keysets[@]}"; do
+    echo "TEST: Detection of invalid keyset '${keyset}'."
+    if keyname=$(android_choose_key "ignored_sha1" "${keyset}"); then
+      : $(( NUM_TEST_FAILURES += 1 ))
+      echo "ERROR: Failed to detect invalid keyset '${keyset}'."
+    else
+      echo "PASS: Detected invalid keyset '${keyset}'."
+    fi
+  done
+}
+
+android_choose_key_test_helper() {
+  local sha1="$1"
+  local keyset="$2"
+  local expected_keyname="$3"
+  local keyname="invalid_key"
+
+  echo "TEST: Detect '${expected_keyname}' key name for '${keyset}' keyset."
+  keyname=$(android_choose_key "${sha1}" "${keyset}")
+  if [[ "${keyname}" != "${expected_keyname}" ]]; then
+    : $(( NUM_TEST_FAILURES += 1 ))
+    echo "ERROR: Incorrect key name '${keyname}' returned."
+  else
+    echo "PASS: Correct key name '${keyname}' returned."
+  fi
+}
+
+test_android_choose_key() {
+  local keyset
+  local expected_keyname
+
+  local keysets=("cheets" "aosp")
+  for keyset in "${keysets[@]}"; do
+    expected_keyname="platform"
+    android_choose_key_test_helper "${platform_sha[${keyset}]}" "${keyset}" \
+      "${expected_keyname}"
+    expected_keyname="media"
+    android_choose_key_test_helper "${media_sha[${keyset}]}" "${keyset}" \
+      "${expected_keyname}"
+    expected_keyname="shared"
+    android_choose_key_test_helper "${shared_sha[${keyset}]}" "${keyset}" \
+      "${expected_keyname}"
+    expected_keyname="releasekey"
+    android_choose_key_test_helper "${release_sha[${keyset}]}" "${keyset}" \
+      "${expected_keyname}"
+  done
+}
+
+build_flavor_test_helper() {
+  local prop_file="${BUILD}/build.prop"
+  local prop_content="$1"
+  local expected_flavor_prop="$2"
+  local flavor_prop=""
+
+  echo "${prop_content}" > "${prop_file}"
+  flavor_prop=$(android_get_build_flavor_prop "${prop_file}")
+  if [[ "${flavor_prop}" != "${expected_flavor_prop}" ]]; then
+    : $(( NUM_TEST_FAILURES += 1 ))
+    echo "ERROR: Incorrect build flavor '${flavor_prop}' returned."
+  else
+    echo "PASS: Correct key name '${flavor_prop}' returned."
+  fi
+  rm "${prop_file}"
+}
+
+test_android_get_build_flavor_prop() {
+  local prop_file="${BUILD}/build.prop"
+  local prop_content=""
+  local flavor_prop=""
+
+  echo "TEST: Extract ro.build.flavor property."
+  prop_content="ro.random.prop=foo
+other.prop=bar
+x=foobar
+ro.build.flavor=cheets_x86-user
+another.prop=barfoo"
+  build_flavor_test_helper "${prop_content}" "cheets_x86-user"
+
+  echo "TEST: Extract single ro.build.flavor property."
+  prop_content="ro.build.flavor=cheets_x86-user"
+  build_flavor_test_helper "${prop_content}" "cheets_x86-user"
+
+  echo "TEST: Avoid commented out ro.build.flavor property."
+  prop_content="ro.random.prop=foo
+other.prop=bar
+x=foobar
+#ro.build.flavor=commented_out
+ro.build.flavor=cheets_x86-user
+another.prop=barfoo"
+  build_flavor_test_helper "${prop_content}" "cheets_x86-user"
+
+  # Missing ro.build.flavor property.
+  echo "TEST: Detect missing ro.build.flavor property."
+  echo "ro.random.prop=foo" > "${prop_file}"
+  if flavor_prop=$(android_get_build_flavor_prop "${prop_file}"); then
+    : $(( NUM_TEST_FAILURES += 1 ))
+    echo "ERROR: Failed to detect missing ro.build.flavor property."
+  else
+    echo "PASS: Detected missing ro.build.flavor property."
+  fi
+  rm "${prop_file}"
+}
+
+choose_signing_keyset_test_helper() {
+  local flavor_prop="$1"
+  local expected_keyset="$2"
+  local keyset=""
+
+  keyset=$(android_choose_signing_keyset "${flavor_prop}")
+  if [[ "${keyset}" != "${expected_keyset}" ]]; then
+    : $(( NUM_TEST_FAILURES += 1 ))
+    echo "ERROR: Incorrect keyset '${keyset}' returned instead of \
+'${expected_keyset}'."
+  else
+    echo "PASS: Correct keyset '${keyset}' returned."
+  fi
+}
+
+choose_signing_keyset_test_invalid_flavors() {
+  local flavor="$1"
+
+  echo "TEST: Detect invalid build flavor '${flavor}'."
+  if android_choose_signing_keyset "${flavor}"; then
+    : $(( NUM_TEST_FAILURES += 1 ))
+    echo "ERROR: Failed to detect invalid build flavor '${flavor}'."
+  else
+    echo "PASS: Detected invalid build flavor '${flavor}'."
+  fi
+}
+
+test_android_choose_signing_keyset() {
+  echo "TEST: Keyset for aosp_cheets build."
+  choose_signing_keyset_test_helper "aosp_cheets_x86-userdebug" "aosp"
+  echo "TEST: Keyset for sdk_google_cheets build."
+  choose_signing_keyset_test_helper "sdk_google_cheets_x86-userdebug" "cheets"
+  echo "TEST: Keyset for cheets_x86 build."
+  choose_signing_keyset_test_helper "cheets_x86-user" "cheets"
+  echo "TEST: Keyset for cheets_arm build."
+  choose_signing_keyset_test_helper "cheets_arm-user" "cheets"
+  echo "TEST: Keyset for cheets_x86_64 build."
+  choose_signing_keyset_test_helper "cheets_x86_64-user" "cheets"
+  echo "TEST: Keyset for userdebug build."
+  choose_signing_keyset_test_helper "cheets_x86-userdebug" "cheets"
+
+  choose_signing_keyset_test_invalid_flavors "aosp"
+  choose_signing_keyset_test_invalid_flavors "cheets"
+  choose_signing_keyset_test_invalid_flavors ""
+  choose_signing_keyset_test_invalid_flavors " "
+}
+
+main() {
+  if [[ $# -ne 0 ]]; then
+    echo "FAIL: unexpected arguments '$@'."
+    return 1
+  fi
+
+  BUILD=$(mktemp -d)
+  echo "Setting temporary build directory as '${BUILD}'."
+
+  test_android_choose_key_invalid_keyset
+  test_android_choose_key
+  test_android_get_build_flavor_prop
+  test_android_choose_signing_keyset
+
+  echo "Deleting temporary build directory '${BUILD}'."
+  rmdir "${BUILD}"
+
+  if [[ ${NUM_TEST_FAILURES} -gt 0 ]]; then
+    echo "FAIL: found ${NUM_TEST_FAILURES} failed :(."
+    return 1
+  fi
+  echo "PASS: all tests passed :)."
+  return 0
+}
+
+# Global incremented by each test when they fail.
+NUM_TEST_FAILURES=0
+main "$@"