package_to_container: create a signed app

In addition to updating package_to_container and the generated
config.json files (includes changes from CL:417097), output the
manifest and sign it.

The layout is:
  manifest.json - file w/config.json & rootfs hashes
  manifest.json.sig - signature of manifest.json

By default we use the devkey from vboot.

BUG=chromium:660209
TEST=run_oci only runs containers with a valid manifest.json{,.sig}
CQ-DEPEND=CL:427498

Change-Id: I3f570ade96e267b420a4609919ebc3af3c7cdc5b
Reviewed-on: https://chromium-review.googlesource.com/415231
Commit-Ready: Mike Frysinger <vapier@chromium.org>
Tested-by: Mike Frysinger <vapier@chromium.org>
Reviewed-by: Mike Frysinger <vapier@chromium.org>
diff --git a/generic_container_files/config.json b/generic_container_files/config.json
index d5eedc8..87dd2f6 100644
--- a/generic_container_files/config.json
+++ b/generic_container_files/config.json
@@ -1,78 +1,97 @@
 {
-	"version": "0.2.0",
+	"ociVersion": "1.0.0-rc1",
 	"platform": {
-		"os": "linux"
+		"os": "linux",
+		"arch": "all"
 	},
 	"process": {
+		"terminal": @TERMINAL@,
 		"user": {
-			"uid": 1000,
-			"gid": 1000,
-			"additionalGids": null
+			"uid": @USER_UID@,
+			"gid": @USER_GID@
 		},
 		"args": [
-			":"
-		]
+			"@ARGV@"
+		],
+		"cwd": "/"
 	},
 	"root": {
-		"path": "root"
+		"path": "rootfs",
+		"readonly": false
 	},
-	"hostname": "generic_container",
+	"hostname": "@APP_NAME@",
 	"mounts": [
 		{
-			"name": "dev",
-			"path": "/dev"
+			"destination": "/",
+			"type": "squashfs",
+			"source": "@APP_NAME@.squashfs",
+			"options": [
+				"ro",
+				"loop",
+				"nodev",
+				"nosuid",
+				"dm=@VERITY@"
+			]
 		},
 		{
-			"name": "dev_bus",
-			"path": "/dev/bus"
+			"destination": "/proc",
+			"type": "proc",
+			"source": "proc",
+			"options": [
+				"nosuid",
+				"noexec",
+				"nodev"
+			]
 		},
 		{
-			"name": "dev_bus_usb",
-			"path": "/dev/bus/usb"
-		},
-		{
-			"name": "dev_null",
-			"path": "/dev/null"
-		},
-		{
-			"name": "dev_random",
-			"path": "/dev/random"
-		},
-		{
-			"name": "dev_urandom",
-			"path": "/dev/urandom"
-		},
-		{
-			"name": "proc",
-			"path": "/proc"
-		},
-		{
-			"name": "run_broker_service",
-			"path": "/run/broker_service"
-		},
-		{
-			"name": "sys",
-			"path": "/sys"
-		},
-		{
-			"name": "sys_bus",
-			"path": "/sys/bus"
-		},
-		{
-			"name": "sys_bus_usb",
-			"path": "/sys/bus/usb"
-		},
-		{
-			"name": "sys_bus_usb_devices",
-			"path": "/sys/bus/usb/devices"
-		},
-		{
-			"name": "sys_devices",
-			"path": "/sys/devices"
-		},
-		{
-			"name": "tmp",
-			"path": "/tmp"
+			"destination": "/dev",
+			"type": "tmpfs",
+			"source": "tmpfs",
+			"options": [
+				"mode=755",
+				"nosuid",
+				"noexec"
+			]
 		}
-	]
+	],
+	"hooks": {},
+	"linux": {
+		"namespaces": [
+			{
+				"type": "cgroup"
+			},
+			{
+				"type": "pid"
+			},
+			{
+				"type": "network"
+			},
+			{
+				"type": "ipc"
+			},
+			{
+				"type": "user"
+			},
+			{
+				"type": "uts"
+			},
+			{
+				"type": "mount"
+			}
+		],
+		"uidMappings": [
+			{
+				"hostID": @USER_UID@,
+				"containerID": 0,
+				"size": 1
+			}
+		],
+		"gidMappings": [
+			{
+				"hostID": @USER_GID@,
+				"containerID": 0,
+				"size": 1
+			}
+		]
+	}
 }
diff --git a/generic_container_files/runtime.json b/generic_container_files/runtime.json
deleted file mode 100644
index 20e327b..0000000
--- a/generic_container_files/runtime.json
+++ /dev/null
@@ -1,126 +0,0 @@
-{
-	"mounts": {
-		"dev": {
-			"type": "tmpfs",
-			"source": "tmpfs",
-			"options": [
-				"nosuid",
-				"mode=0755"
-			]
-		},
-		"dev_bus": {
-			"type": "tmpfs",
-			"source": "tmpfs",
-			"options": [
-				"nosuid",
-				"mode=0755"
-			]
-		},
-		"dev_bus_usb": {
-			"type": "bind",
-			"source": "/dev/bus/usb",
-			"options": [
-				"bind"
-			]
-		},
-		"dev_random": {
-			"type": "bind",
-			"source": "/dev/random",
-			"options": [
-				"bind"
-			]
-		},
-		"dev_urandom": {
-			"type": "bind",
-			"source": "/dev/urandom",
-			"options": [
-				"bind"
-			]
-		},
-		"dev_null": {
-			"type": "bind",
-			"source": "/dev/null",
-			"options": [
-				"bind"
-			]
-		},
-		"sys": {
-			"type": "tmpfs",
-			"source": "tmpfs",
-			"options": [
-				"nosuid",
-				"mode=0755"
-			]
-		},
-		"proc": {
-			"type": "proc",
-			"source": "proc",
-			"options": [
-				"noexec",
-				"nodev",
-				"nosuid"
-			]
-		},
-		"run": {
-			"type": "tmpfs",
-			"source": "tmpfs",
-			"options": [
-				"nosuid",
-				"mode=0755"
-			]
-		},
-		"run_broker_service" : {
-			"type": "bind",
-			"source": "/run/broker_service",
-			"options": [
-				"noexec",
-				"nodev",
-				"bind"
-			]
-		},
-		"sys_bus": {
-			"type": "tmpfs",
-			"source": "tmpfs",
-			"options": [
-				"nosuid",
-				"mode=0755"
-			]
-		},
-		"sys_bus_usb": {
-			"type": "tmpfs",
-			"source": "tmpfs",
-			"options": [
-				"nosuid",
-				"mode=0755"
-			]
-		},
-		"sys_bus_usb_devices": {
-			"type": "bind",
-			"source": "/sys/bus/usb/devices",
-			"options": [
-				"bind"
-			]
-		},
-		"sys_devices": {
-			"type": "bind",
-			"source": "/sys/devices",
-			"options": [
-				"bind"
-			]
-		},
-		"tmp": {
-			"type": "tmpfs",
-			"source": "tmpfs",
-			"options": [
-				"nosuid",
-				"mode=0755"
-			]
-		}
-	},
-	"linux": {
-		"uidMappings": "0 1000 1",
-		"gidMappings": "0 1000 1",
-		"devices": [],
-		"altSysCallTable": "third_party"
-	}
-}
diff --git a/package_to_container b/package_to_container
index 8fec90f..4dcaa1e 100755
--- a/package_to_container
+++ b/package_to_container
@@ -7,92 +7,187 @@
 SCRIPT_ROOT=$(dirname $(readlink -f "$0"))
 . "${SCRIPT_ROOT}/build_library/build_common.sh" || exit 1
 
+assert_inside_chroot "$@"
+
 DEFINE_string board "${DEFAULT_BOARD}" \
   "The board to build an image for."
 DEFINE_string package "" \
   "Package whose bare minimum deps the final container should have."
-DEFINE_string name "" \
+DEFINE_string name "example" \
   "Name of the container."
 DEFINE_string extra "" \
   "Comma separated extra packages to be included in the final image."
+DEFINE_string argv "" \
+  "The command (and args) to run inside the container."
+
+FLAGS_HELP="USAGE: $(basename "$0") [flags]
+
+The --package and --argv options are required.
+
+To build a container for fastboot, you might want something like:
+$ $(basename "$0") --package dev-util/android-tools --argv /usr/bin/fastboot
+"
 
 FLAGS "$@" || exit 1
 eval set -- "${FLAGS_ARGV}"
 switch_to_strict_mode
 
+# Sign the manifest.json file.
+sign_manifest() {
+  "${VBOOT_SIGNING_DIR}"/sign_official_build.sh \
+    oci-container "$1" "${VBOOT_DEVKEYS_DIR}" "$1"
+}
+
+# Generate the manifest.json for this container.
+generate_manifest() {
+  local pkg_name="$1"
+  local output="$2"
+  local manifest="${output}/manifest.json"
+
+  (
+    hash() {
+      local file="$1"
+      local comma="$2"
+      # The Chromium base library does not support dots in dict keys.  They
+      # use it as a short hand to access children dicts.  Normalize them.
+      local dict_key="${file//./_}"
+      local size sha
+
+      size=$(stat -c %s "${file}")
+      sha=$(sha256sum <"${file}" | awk '{print $1}')
+
+      printf '  "%s": {\n    "size": %s,\n    "sha256": "%s"\n  }%s\n' \
+        "${dict_key}" "${size}" "${sha}" "${comma}"
+    }
+
+    cd "${output}"
+    printf '{\n"version": 1,\n"files": {\n'
+    hash "config.json" ","
+    hash "${pkg_name}.squashfs" ""
+    printf '}\n}\n'
+  ) >"${manifest}"
+
+  # Sanity check the generated manifest.
+  python -mjson.tool <"${manifest}" >/dev/null
+
+  sign_manifest "${output}"
+}
+
+# Sign the specified disk image using verity so we can load it with dm-verity
+# at runtime.  We don't allow algorithm selection -- sha1 should be good enough
+# for everyone! :)
+# Note: We write the verity command line to the "verity" variable as an output.
+sign_disk_image() {
+  local img="$1"
+  local hashtree="${img}.hashtree"
+  # TODO: Add "salt=random" here.
+  verity=$(verity mode=create alg=sha256 payload="${img}" \
+                  hashtree="${hashtree}")
+  verity=$(echo "${verity}" | sed -E -e 's:(ROOT|HASH)_DEV:@DEV@:g')
+  cat "${hashtree}" >>"${img}"
+  rm "${hashtree}"
+}
+
 # Produce the container image.
 build_container_image() {
   local pkg_name="$1"
   local container_name="$2"
-  local ROOTDIR="out"
+  local argv="$3"
+  # For now we hardcode chronos.
+  local container_uid="1000"
+  local container_gid="1000"
+  local output="${PWD}/${container_name}"
+  local ROOTDIR="${output}/rootfs"
 
-  info "Cleaning previously generated files ... "
-  sudo rm -rf "${ROOTDIR}"
+  export INSTALL_MASK="${DEFAULT_INSTALL_MASK}"
 
-  install_with_root_deps "${pkg_name}" "${ROOTDIR}"
+  mkdir -p "${output}"
 
-  local pkg
-  for pkg in $(echo $FLAGS_extra | sed "s/,/ /g"); do
-    install_with_root_deps "${pkg}" "${ROOTDIR}"
-  done
+  install_with_root_deps "${ROOTDIR}" "${pkg_name}" sys-libs/gcc-libs \
+    ${FLAGS_extra//,/ }
 
-  info "Installing libc.so ... "
+  info "Installing C library ... "
   install_libc "${ROOTDIR}"
 
-  info "Installing libcontainer_overrides ... "
-  install_with_no_deps "chromeos-base/libcontainer_overrides" "${ROOTDIR}"
+  info "Cleaning excess files ... "
+  sudo rm -rf "${ROOTDIR}"/var "${ROOTDIR}"/usr/lib*/gconv/ \
+    "${ROOTDIR}"/sbin/ldconfig
+  sudo find "${ROOTDIR}"/ -type d -depth -exec rmdir {} + 2>/dev/null || :
 
   info "Creating top level dirs and socket dirs ... "
-  sudo mkdir -p "${ROOTDIR}"/{dev,proc,root,sys,home/user,config}
-  sudo mkdir -p "${ROOTDIR}"/run/broker_service
-
-  info "Adding runtime.json and config.json files ... "
-  sudo cp generic_container_files/config.json "${ROOTDIR}/config"
-  sudo cp generic_container_files/runtime.json "${ROOTDIR}/config"
+  sudo mkdir -p "${ROOTDIR}"/{dev,proc,root,sys,home/user,run,tmp,var}
 
   info "Generating squashfs file ... "
-  mksquashfs "${ROOTDIR}" "${container_name}.sqsh"
+  local args=(
+    -all-root
+    -noappend
+  )
+  local img="${output}/${container_name}.squashfs"
+  # We need to run through sudo because some files might be read-only by root.
+  sudo mksquashfs "${ROOTDIR}" "${img}" "${args[@]}"
+  sudo chown $(id -u):$(id -g) "${img}"
+  info "Signing squashfs file ... "
+  local verity
+  sign_disk_image "${img}"
+
+  info "Adding config.json ... "
+  sed \
+    -e "s:@APP_NAME@:${container_name}:g" \
+    -e "s:@TERMINAL@:true:g" \
+    -e "s:@USER_UID@:${container_uid}:g" \
+    -e "s:@USER_GID@:${container_gid}:g" \
+    -e "s:@ARGV@:${argv// /\", \"}:g" \
+    -e "s:@VERITY@:${verity}:" \
+    generic_container_files/config.json >"${output}/config.json"
+
+  info "Generating manifest ..."
+  generate_manifest "${container_name}" "${output}"
 
   info "Cleaning up ... "
   sudo rm -rf "${ROOTDIR}"
+  mkdir "${ROOTDIR}"
+}
+
+run_emerge() {
+  emerge-${BOARD} \
+    --quiet --jobs ${NUM_JOBS} \
+    --usepkgonly \
+    "$@"
 }
 
 # Normal emerge.
 install_with_no_deps() {
-  local package_to_install="$1"
-  local root_dir="$2"
-  info "Installing '${package_to_install}' with no deps ... "
-  emerge-${BOARD} "${package_to_install}" --nodeps --usepkgonly \
-    --root="${root_dir}" --quiet
+  local root_dir="$1"
+  shift
+  info "Installing '$*' with no deps ... "
+  run_emerge --root="${root_dir}" "$@" --nodeps
 }
 
 # Emerge with root deps.
 install_with_root_deps() {
-  local package_to_install="$1"
-  local root_dir="$2"
-  info "Installing '${package_to_install}' with root deps ... "
-  emerge-${BOARD} "${package_to_install}" --root-deps=rdeps --usepkgonly \
-    --root="${root_dir}" --quiet
+  local root_dir="$1"
+  shift
+  info "Installing '$*' with root deps ... "
+  run_emerge --root="${root_dir}" "$@" --root-deps=rdeps
 }
 
 main() {
   . "${BUILD_LIBRARY_DIR}/board_options.sh" || exit 1
   . "${BUILD_LIBRARY_DIR}/base_image_util.sh" || exit 1
 
-  if [[ -n "${FLAGS_package}" ]]; then
-    if [[ -n "${FLAGS_name}" ]]; then
-      local container_name="${FLAGS_name}"
-    else
-      local container_name="${FLAGS_package}"
-    fi
-    if [[ -f "${container_name}.sqsh" ]]; then
-      die_notrace "File already exists : ${container_name}.sqsh"
-    fi
-    info "Building container '${container_name}' for ${FLAGS_package} ..."
-    build_container_image "${FLAGS_package}" "${container_name}"
-  else
+  if [[ -z "${FLAGS_argv}" ]]; then
+    die_notrace "--argv is needed."
+  fi
+  if [[ -z "${FLAGS_package}" ]]; then
     die_notrace "--package is needed."
   fi
+
+  local container_name="${FLAGS_name}"
+  if [[ -d "${container_name}" ]]; then
+    die_notrace "Output dir already exists : ${container_name}/"
+  fi
+  info "Building container '${container_name}' for ${FLAGS_package} ..."
+  build_container_image "${FLAGS_package}" "${container_name}" "${FLAGS_argv}"
 }
 
 main "$@"