Issue #83: support init_boot.img
parent
1dd865adf4
commit
78105c6b72
@ -0,0 +1,117 @@
|
|||||||
|
// Copyright (C) 2022 The Android Open Source Project
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package {
|
||||||
|
default_applicable_licenses: ["Android-Apache-2.0"],
|
||||||
|
}
|
||||||
|
|
||||||
|
python_binary_host {
|
||||||
|
name: "certify_bootimg",
|
||||||
|
defaults: ["mkbootimg_defaults"],
|
||||||
|
main: "certify_bootimg.py",
|
||||||
|
srcs: [
|
||||||
|
"certify_bootimg.py",
|
||||||
|
"generate_gki_certificate.py",
|
||||||
|
],
|
||||||
|
required: [
|
||||||
|
"avbtool",
|
||||||
|
"unpack_bootimg",
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
python_test_host {
|
||||||
|
name: "certify_bootimg_test",
|
||||||
|
defaults: ["mkbootimg_defaults"],
|
||||||
|
main: "certify_bootimg_test.py",
|
||||||
|
srcs: [
|
||||||
|
"certify_bootimg_test.py",
|
||||||
|
],
|
||||||
|
data: [
|
||||||
|
":avbtool",
|
||||||
|
":certify_bootimg",
|
||||||
|
":mkbootimg",
|
||||||
|
":unpack_bootimg",
|
||||||
|
"testdata/*",
|
||||||
|
],
|
||||||
|
test_options: {
|
||||||
|
unit_test: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
python_binary_host {
|
||||||
|
name: "generate_gki_certificate",
|
||||||
|
defaults: ["mkbootimg_defaults"],
|
||||||
|
srcs: [
|
||||||
|
"generate_gki_certificate.py",
|
||||||
|
],
|
||||||
|
required: [
|
||||||
|
"avbtool",
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
sh_binary_host {
|
||||||
|
name: "retrofit_gki",
|
||||||
|
src: "retrofit_gki.sh",
|
||||||
|
required: [
|
||||||
|
"avbtool",
|
||||||
|
"mkbootimg",
|
||||||
|
"unpack_bootimg",
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
sh_test_host {
|
||||||
|
name: "retrofit_gki_test",
|
||||||
|
src: "retrofit_gki_test.sh",
|
||||||
|
data: [
|
||||||
|
"retrofit_gki.sh",
|
||||||
|
],
|
||||||
|
data_bins: [
|
||||||
|
"avbtool",
|
||||||
|
"mkbootimg",
|
||||||
|
"unpack_bootimg",
|
||||||
|
],
|
||||||
|
test_suites: [
|
||||||
|
"general-tests",
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
genrule {
|
||||||
|
name: "gki_retrofitting_tools",
|
||||||
|
tools: [
|
||||||
|
"soong_zip",
|
||||||
|
"retrofit_gki",
|
||||||
|
"avbtool",
|
||||||
|
"mkbootimg",
|
||||||
|
"unpack_bootimg",
|
||||||
|
],
|
||||||
|
srcs: [
|
||||||
|
"README.md",
|
||||||
|
],
|
||||||
|
cmd: "STAGE_DIR=$(genDir)/gki_retrofitting_tools && " +
|
||||||
|
"rm -rf $${STAGE_DIR} && mkdir -p $${STAGE_DIR} && " +
|
||||||
|
"cp $(location retrofit_gki) $${STAGE_DIR} && " +
|
||||||
|
"cp $(location avbtool) $${STAGE_DIR} && " +
|
||||||
|
"cp $(location mkbootimg) $${STAGE_DIR} && " +
|
||||||
|
"cp $(location unpack_bootimg) $${STAGE_DIR} && " +
|
||||||
|
"cp $(in) $${STAGE_DIR} && " +
|
||||||
|
"$(location soong_zip) -o $(out) -C $(genDir) -D $${STAGE_DIR}",
|
||||||
|
out: [
|
||||||
|
"gki_retrofitting_tools.zip",
|
||||||
|
],
|
||||||
|
dist: {
|
||||||
|
targets: [
|
||||||
|
"gki_retrofitting_tools",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}
|
||||||
@ -0,0 +1,36 @@
|
|||||||
|
#
|
||||||
|
# Copyright (C) 2022 The Android Open Source Project
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
#
|
||||||
|
|
||||||
|
_gsi_gki_product_names := \
|
||||||
|
aosp_arm \
|
||||||
|
aosp_arm64 \
|
||||||
|
aosp_x86 \
|
||||||
|
aosp_x86_64 \
|
||||||
|
gsi_arm \
|
||||||
|
gsi_arm64 \
|
||||||
|
gsi_x86 \
|
||||||
|
gsi_x86_64 \
|
||||||
|
gki_arm64 \
|
||||||
|
gki_x86_64 \
|
||||||
|
|
||||||
|
# Add gki_retrofitting_tools to `m dist` of GSI/GKI for easy pickup.
|
||||||
|
ifneq (,$(filter $(_gsi_gki_product_names),$(TARGET_PRODUCT)))
|
||||||
|
|
||||||
|
droidcore-unbundled: gki_retrofitting_tools
|
||||||
|
|
||||||
|
endif
|
||||||
|
|
||||||
|
_gsi_gki_product_names :=
|
||||||
@ -0,0 +1,74 @@
|
|||||||
|
# GKI boot image retrofitting tools for upgrading devices
|
||||||
|
|
||||||
|
Starting from Android T the GKI boot images consist of the generic `boot.img`
|
||||||
|
and `init_boot.img`. The `boot.img` contains the generic kernel, and
|
||||||
|
`init_boot.img` contains the generic ramdisk.
|
||||||
|
For upgrading devices whose `vendor_boot` partition is non-existent, this tool
|
||||||
|
(or spec) can be used to retrofit a set of Android T GKI `boot`, `init_boot` and
|
||||||
|
OEM `vendor_boot` partition images back into a single boot image containing the
|
||||||
|
GKI kernel plus generic and vendor ramdisks.
|
||||||
|
|
||||||
|
## Retrofitting the boot images
|
||||||
|
|
||||||
|
1. Download the certified GKI `boot.img`.
|
||||||
|
2. Go to the build artifacts page of `aosp_arm64` on `aosp-master` branch on
|
||||||
|
https://ci.android.com/ and download `gki_retrofitting_tools.zip`.
|
||||||
|
3. Unzip and make sure the tool is in `${PATH}`.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
unzip gki_retrofitting_tools.zip
|
||||||
|
export PATH="$(pwd)/gki_retrofitting_tools:${PATH}"
|
||||||
|
# See tool usage:
|
||||||
|
retrofit_gki --help
|
||||||
|
```
|
||||||
|
|
||||||
|
4. Create the retrofitted image. The `--version` argument lets you choose the
|
||||||
|
boot image header version of the retrofitted boot image. Only version 2 is
|
||||||
|
supported at the moment.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
retrofit_gki --boot boot.img --init_boot init_boot.img \
|
||||||
|
--vendor_boot vendor_boot.img --version 2 -o boot.retrofitted.img
|
||||||
|
```
|
||||||
|
|
||||||
|
## Spec of the retrofitted images
|
||||||
|
|
||||||
|
* The SOURCE `boot.img` must be officially certified Android T (or later) GKI.
|
||||||
|
* The DEST retrofitted boot image must not set the security patch level in its
|
||||||
|
header. This is because the SOURCE images might have different SPL value, thus
|
||||||
|
making the boot header SPL of the retrofitted image ill-defined. The SPL value
|
||||||
|
must be defined by the chained vbmeta image of the `boot` partition.
|
||||||
|
* The `boot signature` of the DEST image is the `boot signature` of the DEST
|
||||||
|
`boot.img`.
|
||||||
|
* The DEST retrofitted boot image must pass the `vts_gki_compliance_test`
|
||||||
|
testcase.
|
||||||
|
|
||||||
|
### Retrofit to boot image V2
|
||||||
|
|
||||||
|
* The `kernel` of the DEST image must be from the SOURCE `boot.img`.
|
||||||
|
* The `ramdisk` of the DEST image must be from the SOURCE `vendor_boot.img` and
|
||||||
|
`init_boot.img`. The DEST `ramdisk` is the ramdisk concatenation of the vendor
|
||||||
|
ramdisk and generic ramdisk.
|
||||||
|
* The `recovery dtbo / acpio` must be empty.
|
||||||
|
* The `dtb` of the DEST image must be from the SOURCE `vendor_boot.img`.
|
||||||
|
* The `boot_signature` section must be appended to the end of the boot image,
|
||||||
|
and its size is zero-padded to 16KiB.
|
||||||
|
|
||||||
|
```
|
||||||
|
+---------------------+
|
||||||
|
| boot header | 1 page
|
||||||
|
+---------------------+
|
||||||
|
| kernel | n pages
|
||||||
|
+---------------------+
|
||||||
|
| * vendor ramdisk |
|
||||||
|
| +generic ramdisk | m pages
|
||||||
|
+---------------------+
|
||||||
|
| second stage | o pages
|
||||||
|
+---------------------+
|
||||||
|
| recovery dtbo/acpio | 0 byte
|
||||||
|
+---------------------+
|
||||||
|
| dtb | q pages
|
||||||
|
+---------------------+
|
||||||
|
| * boot signature | 16384 (16K) bytes
|
||||||
|
+---------------------+
|
||||||
|
```
|
||||||
@ -0,0 +1,98 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
#
|
||||||
|
# Copyright (C) 2022 The Android Open Source Project
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
#
|
||||||
|
|
||||||
|
#
|
||||||
|
# Dump boot signature info of a GKI boot image.
|
||||||
|
#
|
||||||
|
|
||||||
|
set -eo errtrace
|
||||||
|
|
||||||
|
die() {
|
||||||
|
echo >&2 "ERROR:" "${@}"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
TEMP_DIR="$(mktemp -d)"
|
||||||
|
readonly TEMP_DIR
|
||||||
|
|
||||||
|
exit_handler() {
|
||||||
|
readonly EXIT_CODE="$?"
|
||||||
|
rm -rf "${TEMP_DIR}" ||:
|
||||||
|
exit "${EXIT_CODE}"
|
||||||
|
}
|
||||||
|
|
||||||
|
trap exit_handler EXIT
|
||||||
|
trap 'die "line ${LINENO}, ${FUNCNAME:-<main>}(): \"${BASH_COMMAND}\" returned \"$?\"" ' ERR
|
||||||
|
|
||||||
|
get_arg() {
|
||||||
|
local arg="$1"
|
||||||
|
shift
|
||||||
|
while [[ "$#" -gt 0 ]]; do
|
||||||
|
if [[ "$1" == "${arg}" ]]; then
|
||||||
|
shift
|
||||||
|
echo "$1"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
shift
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
readonly VBMETA_IMAGE="${TEMP_DIR}/boot.boot_signature"
|
||||||
|
readonly VBMETA_IMAGE_TEMP="${VBMETA_IMAGE}.temp"
|
||||||
|
readonly VBMETA_INFO="${VBMETA_IMAGE}.info"
|
||||||
|
readonly BOOT_IMAGE="${TEMP_DIR}/boot.img"
|
||||||
|
readonly BOOT_IMAGE_DIR="${TEMP_DIR}/boot.unpack_dir"
|
||||||
|
readonly BOOT_IMAGE_ARGS="${TEMP_DIR}/boot.mkbootimg_args"
|
||||||
|
readonly BOOT_SIGNATURE_SIZE=$(( 16 << 10 ))
|
||||||
|
|
||||||
|
[[ -f "$1" ]] ||
|
||||||
|
die "expected one input image"
|
||||||
|
cp "$1" "${BOOT_IMAGE}"
|
||||||
|
|
||||||
|
# This could fail if there already is no AVB footer.
|
||||||
|
avbtool erase_footer --image "${BOOT_IMAGE}" 2>/dev/null ||:
|
||||||
|
|
||||||
|
unpack_bootimg --boot_img "${BOOT_IMAGE}" --out "${BOOT_IMAGE_DIR}" \
|
||||||
|
--format=mkbootimg -0 > "${BOOT_IMAGE_ARGS}"
|
||||||
|
|
||||||
|
declare -a boot_args=()
|
||||||
|
while IFS= read -r -d '' ARG; do
|
||||||
|
boot_args+=("${ARG}")
|
||||||
|
done < "${BOOT_IMAGE_ARGS}"
|
||||||
|
|
||||||
|
BOOT_IMAGE_VERSION="$(get_arg --header_version "${boot_args[@]}")"
|
||||||
|
if [[ "${BOOT_IMAGE_VERSION}" -ge 4 ]] && [[ -f "${BOOT_IMAGE_DIR}/boot_signature" ]]; then
|
||||||
|
cp "${BOOT_IMAGE_DIR}/boot_signature" "${VBMETA_IMAGE}"
|
||||||
|
else
|
||||||
|
tail -c "${BOOT_SIGNATURE_SIZE}" "${BOOT_IMAGE}" > "${VBMETA_IMAGE}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Keep carving out vbmeta image from the boot signature until we fail or EOF.
|
||||||
|
# Failing is fine because there could be padding trailing the boot signature.
|
||||||
|
while avbtool info_image --image "${VBMETA_IMAGE}" --output "${VBMETA_INFO}" 2>/dev/null; do
|
||||||
|
cat "${VBMETA_INFO}"
|
||||||
|
echo
|
||||||
|
|
||||||
|
declare -i H A X
|
||||||
|
H="$(cat "${VBMETA_INFO}" | grep 'Header Block:' | awk '{print $3}')"
|
||||||
|
A="$(cat "${VBMETA_INFO}" | grep 'Authentication Block:' | awk '{print $3}')"
|
||||||
|
X="$(cat "${VBMETA_INFO}" | grep 'Auxiliary Block:' | awk '{print $3}')"
|
||||||
|
vbmeta_size="$(( ${H} + ${A} + ${X} ))"
|
||||||
|
|
||||||
|
tail -c "+$(( ${vbmeta_size} + 1 ))" "${VBMETA_IMAGE}" > "${VBMETA_IMAGE_TEMP}"
|
||||||
|
cp "${VBMETA_IMAGE_TEMP}" "${VBMETA_IMAGE}"
|
||||||
|
done
|
||||||
@ -0,0 +1,248 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
#
|
||||||
|
# Copyright 2022, The Android Open Source Project
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
#
|
||||||
|
|
||||||
|
"""Certify a GKI boot image by generating and appending its boot_signature."""
|
||||||
|
|
||||||
|
from argparse import ArgumentParser
|
||||||
|
import glob
|
||||||
|
import os
|
||||||
|
import shlex
|
||||||
|
import shutil
|
||||||
|
import subprocess
|
||||||
|
import tempfile
|
||||||
|
|
||||||
|
from generate_gki_certificate import generate_gki_certificate
|
||||||
|
|
||||||
|
BOOT_SIGNATURE_SIZE = 16 * 1024
|
||||||
|
|
||||||
|
|
||||||
|
def get_kernel(boot_img):
|
||||||
|
"""Extracts the kernel from |boot_img| and returns it."""
|
||||||
|
with tempfile.TemporaryDirectory() as unpack_dir:
|
||||||
|
unpack_bootimg_cmd = [
|
||||||
|
'unpack_bootimg',
|
||||||
|
'--boot_img', boot_img,
|
||||||
|
'--out', unpack_dir,
|
||||||
|
]
|
||||||
|
subprocess.run(unpack_bootimg_cmd, check=True,
|
||||||
|
stdout=subprocess.DEVNULL)
|
||||||
|
|
||||||
|
with open(os.path.join(unpack_dir, 'kernel'), 'rb') as kernel:
|
||||||
|
kernel_bytes = kernel.read()
|
||||||
|
assert len(kernel_bytes) > 0
|
||||||
|
return kernel_bytes
|
||||||
|
|
||||||
|
|
||||||
|
def add_certificate(boot_img, algorithm, key, extra_args):
|
||||||
|
"""Appends certificates to the end of the boot image.
|
||||||
|
|
||||||
|
This functions appends two certificates to the end of the |boot_img|:
|
||||||
|
the 'boot' certificate and the 'generic_kernel' certificate. The former
|
||||||
|
is to certify the entire |boot_img|, while the latter is to certify
|
||||||
|
the kernel inside the |boot_img|.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def generate_certificate(image, certificate_name):
|
||||||
|
"""Generates the certificate and returns the certificate content."""
|
||||||
|
with tempfile.NamedTemporaryFile() as output_certificate:
|
||||||
|
generate_gki_certificate(
|
||||||
|
image=image, avbtool='avbtool', name=certificate_name,
|
||||||
|
algorithm=algorithm, key=key, salt='d00df00d',
|
||||||
|
additional_avb_args=extra_args, output=output_certificate.name)
|
||||||
|
output_certificate.seek(os.SEEK_SET, 0)
|
||||||
|
return output_certificate.read()
|
||||||
|
|
||||||
|
boot_signature_bytes = b''
|
||||||
|
boot_signature_bytes += generate_certificate(boot_img, 'boot')
|
||||||
|
|
||||||
|
with tempfile.NamedTemporaryFile() as kernel_img:
|
||||||
|
kernel_img.write(get_kernel(boot_img))
|
||||||
|
kernel_img.flush()
|
||||||
|
boot_signature_bytes += generate_certificate(kernel_img.name,
|
||||||
|
'generic_kernel')
|
||||||
|
|
||||||
|
if len(boot_signature_bytes) > BOOT_SIGNATURE_SIZE:
|
||||||
|
raise ValueError(
|
||||||
|
f'boot_signature size must be <= {BOOT_SIGNATURE_SIZE}')
|
||||||
|
boot_signature_bytes += (
|
||||||
|
b'\0' * (BOOT_SIGNATURE_SIZE - len(boot_signature_bytes)))
|
||||||
|
assert len(boot_signature_bytes) == BOOT_SIGNATURE_SIZE
|
||||||
|
|
||||||
|
with open(boot_img, 'ab') as f:
|
||||||
|
f.write(boot_signature_bytes)
|
||||||
|
|
||||||
|
|
||||||
|
def erase_certificate_and_avb_footer(boot_img):
|
||||||
|
"""Erases the boot certificate and avb footer.
|
||||||
|
|
||||||
|
A boot image might already contain a certificate and/or a AVB footer.
|
||||||
|
This function erases these additional metadata from the |boot_img|.
|
||||||
|
"""
|
||||||
|
# Tries to erase the AVB footer first, which may or may not exist.
|
||||||
|
avbtool_cmd = ['avbtool', 'erase_footer', '--image', boot_img]
|
||||||
|
subprocess.run(avbtool_cmd, check=False, stderr=subprocess.DEVNULL)
|
||||||
|
assert os.path.getsize(boot_img) > 0
|
||||||
|
|
||||||
|
# No boot signature to erase, just return.
|
||||||
|
if os.path.getsize(boot_img) <= BOOT_SIGNATURE_SIZE:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Checks if the last 16K is a boot signature, then erases it.
|
||||||
|
with open(boot_img, 'rb') as image:
|
||||||
|
image.seek(-BOOT_SIGNATURE_SIZE, os.SEEK_END)
|
||||||
|
boot_signature = image.read(BOOT_SIGNATURE_SIZE)
|
||||||
|
assert len(boot_signature) == BOOT_SIGNATURE_SIZE
|
||||||
|
|
||||||
|
with tempfile.NamedTemporaryFile() as signature_tmpfile:
|
||||||
|
signature_tmpfile.write(boot_signature)
|
||||||
|
signature_tmpfile.flush()
|
||||||
|
avbtool_info_cmd = [
|
||||||
|
'avbtool', 'info_image', '--image', signature_tmpfile.name]
|
||||||
|
result = subprocess.run(avbtool_info_cmd, check=False,
|
||||||
|
stdout=subprocess.DEVNULL,
|
||||||
|
stderr=subprocess.DEVNULL)
|
||||||
|
has_boot_signature = (result.returncode == 0)
|
||||||
|
|
||||||
|
if has_boot_signature:
|
||||||
|
new_file_size = os.path.getsize(boot_img) - BOOT_SIGNATURE_SIZE
|
||||||
|
os.truncate(boot_img, new_file_size)
|
||||||
|
|
||||||
|
assert os.path.getsize(boot_img) > 0
|
||||||
|
|
||||||
|
|
||||||
|
def get_avb_image_size(image):
|
||||||
|
"""Returns the image size if there is a AVB footer, else return zero."""
|
||||||
|
|
||||||
|
avbtool_info_cmd = ['avbtool', 'info_image', '--image', image]
|
||||||
|
result = subprocess.run(avbtool_info_cmd, check=False,
|
||||||
|
stdout=subprocess.DEVNULL,
|
||||||
|
stderr=subprocess.DEVNULL)
|
||||||
|
|
||||||
|
if result.returncode == 0:
|
||||||
|
return os.path.getsize(image)
|
||||||
|
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
def add_avb_footer(image, partition_size):
|
||||||
|
"""Appends a AVB hash footer to the image."""
|
||||||
|
|
||||||
|
avbtool_cmd = ['avbtool', 'add_hash_footer', '--image', image,
|
||||||
|
'--partition_name', 'boot']
|
||||||
|
|
||||||
|
if partition_size:
|
||||||
|
avbtool_cmd.extend(['--partition_size', str(partition_size)])
|
||||||
|
else:
|
||||||
|
avbtool_cmd.extend(['--dynamic_partition_size'])
|
||||||
|
|
||||||
|
subprocess.check_call(avbtool_cmd)
|
||||||
|
|
||||||
|
|
||||||
|
def load_dict_from_file(path):
|
||||||
|
"""Loads key=value pairs from |path| and returns a dict."""
|
||||||
|
d = {}
|
||||||
|
with open(path, 'r', encoding='utf-8') as f:
|
||||||
|
for line in f:
|
||||||
|
line = line.strip()
|
||||||
|
if not line or line.startswith('#'):
|
||||||
|
continue
|
||||||
|
if '=' in line:
|
||||||
|
name, value = line.split('=', 1)
|
||||||
|
d[name] = value
|
||||||
|
return d
|
||||||
|
|
||||||
|
|
||||||
|
def parse_cmdline():
|
||||||
|
"""Parse command-line options."""
|
||||||
|
parser = ArgumentParser(add_help=True)
|
||||||
|
|
||||||
|
# Required args.
|
||||||
|
input_group = parser.add_mutually_exclusive_group(required=True)
|
||||||
|
input_group.add_argument(
|
||||||
|
'--boot_img', help='path to the boot image to certify')
|
||||||
|
input_group.add_argument(
|
||||||
|
'--boot_img_zip', help='path to the boot-img-*.zip archive to certify')
|
||||||
|
|
||||||
|
parser.add_argument('--algorithm', required=True,
|
||||||
|
help='signing algorithm for the certificate')
|
||||||
|
parser.add_argument('--key', required=True,
|
||||||
|
help='path to the RSA private key')
|
||||||
|
parser.add_argument('-o', '--output', required=True,
|
||||||
|
help='output file name')
|
||||||
|
|
||||||
|
# Optional args.
|
||||||
|
parser.add_argument('--extra_args', default=[], action='append',
|
||||||
|
help='extra arguments to be forwarded to avbtool')
|
||||||
|
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
extra_args = []
|
||||||
|
for a in args.extra_args:
|
||||||
|
extra_args.extend(shlex.split(a))
|
||||||
|
args.extra_args = extra_args
|
||||||
|
|
||||||
|
return args
|
||||||
|
|
||||||
|
|
||||||
|
def certify_bootimg(boot_img, output_img, algorithm, key, extra_args):
|
||||||
|
"""Certify a GKI boot image by generating and appending a boot_signature."""
|
||||||
|
with tempfile.TemporaryDirectory() as temp_dir:
|
||||||
|
boot_tmp = os.path.join(temp_dir, 'boot.tmp')
|
||||||
|
shutil.copy2(boot_img, boot_tmp)
|
||||||
|
|
||||||
|
erase_certificate_and_avb_footer(boot_tmp)
|
||||||
|
add_certificate(boot_tmp, algorithm, key, extra_args)
|
||||||
|
|
||||||
|
avb_partition_size = get_avb_image_size(boot_img)
|
||||||
|
add_avb_footer(boot_tmp, avb_partition_size)
|
||||||
|
|
||||||
|
# We're done, copy the temp image to the final output.
|
||||||
|
shutil.copy2(boot_tmp, output_img)
|
||||||
|
|
||||||
|
|
||||||
|
def certify_bootimg_zip(boot_img_zip, output_zip, algorithm, key, extra_args):
|
||||||
|
"""Similar to certify_bootimg(), but for a zip archive of boot images."""
|
||||||
|
with tempfile.TemporaryDirectory() as unzip_dir:
|
||||||
|
shutil.unpack_archive(boot_img_zip, unzip_dir)
|
||||||
|
|
||||||
|
info_dict = load_dict_from_file(os.path.join(unzip_dir, 'gki-info.txt'))
|
||||||
|
extra_args.extend(shlex.split(info_dict['certify_bootimg_extra_args']))
|
||||||
|
|
||||||
|
for boot_img in glob.glob(os.path.join(unzip_dir, 'boot-*.img')):
|
||||||
|
print(f'Certifying {os.path.basename(boot_img)} ...')
|
||||||
|
certify_bootimg(boot_img=boot_img, output_img=boot_img,
|
||||||
|
algorithm=algorithm, key=key, extra_args=extra_args)
|
||||||
|
|
||||||
|
print(f'Making certified archive: {output_zip}')
|
||||||
|
archive_base_name = os.path.splitext(output_zip)[0]
|
||||||
|
shutil.make_archive(archive_base_name, 'zip', unzip_dir)
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""Parse arguments and certify the boot image."""
|
||||||
|
args = parse_cmdline()
|
||||||
|
|
||||||
|
if args.boot_img_zip:
|
||||||
|
certify_bootimg_zip(args.boot_img_zip, args.output, args.algorithm,
|
||||||
|
args.key, args.extra_args)
|
||||||
|
else:
|
||||||
|
certify_bootimg(args.boot_img, args.output, args.algorithm,
|
||||||
|
args.key, args.extra_args)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
@ -0,0 +1,653 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
#
|
||||||
|
# Copyright 2022, The Android Open Source Project
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
|
||||||
|
"""Tests certify_bootimg."""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
import glob
|
||||||
|
import os
|
||||||
|
import random
|
||||||
|
import shutil
|
||||||
|
import struct
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import tempfile
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
BOOT_SIGNATURE_SIZE = 16 * 1024
|
||||||
|
|
||||||
|
TEST_KERNEL_CMDLINE = (
|
||||||
|
'printk.devkmsg=on firmware_class.path=/vendor/etc/ init=/init '
|
||||||
|
'kfence.sample_interval=500 loop.max_part=7 bootconfig'
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def generate_test_file(pathname, size, seed=None):
|
||||||
|
"""Generates a gibberish-filled test file and returns its pathname."""
|
||||||
|
random.seed(os.path.basename(pathname) if seed is None else seed)
|
||||||
|
with open(pathname, 'wb') as file:
|
||||||
|
file.write(random.randbytes(size))
|
||||||
|
return pathname
|
||||||
|
|
||||||
|
|
||||||
|
def generate_test_boot_image(boot_img, kernel_size=4096, seed='kernel',
|
||||||
|
avb_partition_size=None):
|
||||||
|
"""Generates a test boot.img without a ramdisk."""
|
||||||
|
with tempfile.NamedTemporaryFile() as kernel_tmpfile:
|
||||||
|
generate_test_file(kernel_tmpfile.name, kernel_size, seed)
|
||||||
|
kernel_tmpfile.flush()
|
||||||
|
|
||||||
|
mkbootimg_cmds = [
|
||||||
|
'mkbootimg',
|
||||||
|
'--header_version', '4',
|
||||||
|
'--kernel', kernel_tmpfile.name,
|
||||||
|
'--cmdline', TEST_KERNEL_CMDLINE,
|
||||||
|
'--os_version', '12.0.0',
|
||||||
|
'--os_patch_level', '2022-03',
|
||||||
|
'--output', boot_img,
|
||||||
|
]
|
||||||
|
subprocess.check_call(mkbootimg_cmds)
|
||||||
|
|
||||||
|
if avb_partition_size:
|
||||||
|
avbtool_cmd = ['avbtool', 'add_hash_footer', '--image', boot_img,
|
||||||
|
'--partition_name', 'boot',
|
||||||
|
'--partition_size', str(avb_partition_size)]
|
||||||
|
subprocess.check_call(avbtool_cmd)
|
||||||
|
|
||||||
|
|
||||||
|
def generate_test_boot_image_archive(output_zip, boot_img_info):
|
||||||
|
"""Generates a zip archive of test boot images.
|
||||||
|
|
||||||
|
It also adds a file gki-info.txt, which contains additional settings for
|
||||||
|
for `certify_bootimg --extra_args`.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
output_zip: the output zip archive, e.g., /path/to/boot-img.zip.
|
||||||
|
boot_img_info: a list of (boot_image_name, kernel_size,
|
||||||
|
partition_size) tuples. e.g.,
|
||||||
|
[('boot-1.0.img', 4096, 4 * 1024),
|
||||||
|
('boot-2.0.img', 8192, 8 * 1024)].
|
||||||
|
"""
|
||||||
|
with tempfile.TemporaryDirectory() as temp_out_dir:
|
||||||
|
for name, kernel_size, partition_size in boot_img_info:
|
||||||
|
boot_img = os.path.join(temp_out_dir, name)
|
||||||
|
generate_test_boot_image(boot_img=boot_img,
|
||||||
|
kernel_size=kernel_size,
|
||||||
|
seed=name,
|
||||||
|
avb_partition_size=partition_size)
|
||||||
|
|
||||||
|
gki_info = os.path.join(temp_out_dir, 'gki-info.txt')
|
||||||
|
with open(gki_info, 'w', encoding='utf-8') as f:
|
||||||
|
f.write('certify_bootimg_extra_args='
|
||||||
|
'--prop KERNEL_RELEASE:5.10.42'
|
||||||
|
'-android13-0-00544-ged21d463f856 '
|
||||||
|
'--prop BRANCH:android13-5.10-2022-05 '
|
||||||
|
'--prop BUILD_NUMBER:ab8295296 '
|
||||||
|
'--prop SPACE:"nice to meet you"\n')
|
||||||
|
|
||||||
|
archive_base_name = os.path.splitext(output_zip)[0]
|
||||||
|
shutil.make_archive(archive_base_name, 'zip', temp_out_dir)
|
||||||
|
|
||||||
|
|
||||||
|
def has_avb_footer(image):
|
||||||
|
"""Returns true if the image has a avb footer."""
|
||||||
|
|
||||||
|
avbtool_info_cmd = ['avbtool', 'info_image', '--image', image]
|
||||||
|
result = subprocess.run(avbtool_info_cmd, check=False,
|
||||||
|
stdout=subprocess.DEVNULL,
|
||||||
|
stderr=subprocess.DEVNULL)
|
||||||
|
|
||||||
|
return result.returncode == 0
|
||||||
|
|
||||||
|
|
||||||
|
def get_vbmeta_size(vbmeta_bytes):
|
||||||
|
"""Returns the total size of a AvbVBMeta image."""
|
||||||
|
|
||||||
|
# Keep in sync with |AvbVBMetaImageHeader|.
|
||||||
|
AVB_MAGIC = b'AVB0' # pylint: disable=C0103
|
||||||
|
AVB_VBMETA_IMAGE_HEADER_SIZE = 256 # pylint: disable=C0103
|
||||||
|
FORMAT_STRING = ( # pylint: disable=C0103
|
||||||
|
'!4s2L' # magic, 2 x version.
|
||||||
|
'2Q' # 2 x block size: Authentication and Auxiliary blocks.
|
||||||
|
)
|
||||||
|
|
||||||
|
if len(vbmeta_bytes) < struct.calcsize(FORMAT_STRING):
|
||||||
|
return 0
|
||||||
|
|
||||||
|
data = vbmeta_bytes[:struct.calcsize(FORMAT_STRING)]
|
||||||
|
(magic, _, _,
|
||||||
|
authentication_block_size,
|
||||||
|
auxiliary_data_block_size) = struct.unpack(FORMAT_STRING, data)
|
||||||
|
|
||||||
|
if magic == AVB_MAGIC:
|
||||||
|
return (AVB_VBMETA_IMAGE_HEADER_SIZE +
|
||||||
|
authentication_block_size +
|
||||||
|
auxiliary_data_block_size)
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
def extract_boot_signatures(boot_img, output_dir):
|
||||||
|
"""Extracts the boot signatures of a boot image.
|
||||||
|
|
||||||
|
This functions extracts the boot signatures of |boot_img| as:
|
||||||
|
- |output_dir|/boot_signature1
|
||||||
|
- |output_dir|/boot_signature2
|
||||||
|
"""
|
||||||
|
|
||||||
|
boot_img_copy = os.path.join(output_dir, 'boot_image_copy')
|
||||||
|
shutil.copy2(boot_img, boot_img_copy)
|
||||||
|
avbtool_cmd = ['avbtool', 'erase_footer', '--image', boot_img_copy]
|
||||||
|
subprocess.run(avbtool_cmd, check=False, stderr=subprocess.DEVNULL)
|
||||||
|
|
||||||
|
# The boot signature is assumed to be at the end of boot image, after
|
||||||
|
# the AVB footer is erased.
|
||||||
|
with open(boot_img_copy, 'rb') as image:
|
||||||
|
image.seek(-BOOT_SIGNATURE_SIZE, os.SEEK_END)
|
||||||
|
boot_signature_bytes = image.read(BOOT_SIGNATURE_SIZE)
|
||||||
|
assert len(boot_signature_bytes) == BOOT_SIGNATURE_SIZE
|
||||||
|
os.unlink(boot_img_copy)
|
||||||
|
|
||||||
|
num_signatures = 0
|
||||||
|
while True:
|
||||||
|
next_signature_size = get_vbmeta_size(boot_signature_bytes)
|
||||||
|
if next_signature_size <= 0:
|
||||||
|
break
|
||||||
|
|
||||||
|
num_signatures += 1
|
||||||
|
next_signature = boot_signature_bytes[:next_signature_size]
|
||||||
|
output_path = os.path.join(
|
||||||
|
output_dir, 'boot_signature' + str(num_signatures))
|
||||||
|
with open(output_path, 'wb') as output:
|
||||||
|
output.write(next_signature)
|
||||||
|
|
||||||
|
# Moves to the next signature.
|
||||||
|
boot_signature_bytes = boot_signature_bytes[next_signature_size:]
|
||||||
|
|
||||||
|
|
||||||
|
def extract_boot_archive_with_signatures(boot_img_zip, output_dir):
|
||||||
|
"""Extracts boot images and signatures of a boot images archive.
|
||||||
|
|
||||||
|
Suppose there are two boot images in |boot_img_zip|: boot-1.0.img
|
||||||
|
and boot-2.0.img. This function then extracts each boot-*.img and
|
||||||
|
their signatures as:
|
||||||
|
- |output_dir|/boot-1.0.img
|
||||||
|
- |output_dir|/boot-2.0.img
|
||||||
|
- |output_dir|/boot-1.0/boot_signature1
|
||||||
|
- |output_dir|/boot-1.0/boot_signature2
|
||||||
|
- |output_dir|/boot-2.0/boot_signature1
|
||||||
|
- |output_dir|/boot-2.0/boot_signature2
|
||||||
|
"""
|
||||||
|
shutil.unpack_archive(boot_img_zip, output_dir)
|
||||||
|
for boot_img in glob.glob(os.path.join(output_dir, 'boot-*.img')):
|
||||||
|
img_name = os.path.splitext(os.path.basename(boot_img))[0]
|
||||||
|
signature_output_dir = os.path.join(output_dir, img_name)
|
||||||
|
os.mkdir(signature_output_dir, 0o777)
|
||||||
|
extract_boot_signatures(boot_img, signature_output_dir)
|
||||||
|
|
||||||
|
|
||||||
|
class CertifyBootimgTest(unittest.TestCase):
|
||||||
|
"""Tests the functionalities of certify_bootimg."""
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
# Saves the test executable directory so that relative path references
|
||||||
|
# to test dependencies don't rely on being manually run from the
|
||||||
|
# executable directory.
|
||||||
|
# With this, we can just open "./testdata/testkey_rsa2048.pem" in the
|
||||||
|
# following tests with subprocess.run(..., cwd=self._exec_dir, ...).
|
||||||
|
self._exec_dir = os.path.abspath(os.path.dirname(sys.argv[0]))
|
||||||
|
|
||||||
|
# Set self.maxDiff to None to see full diff in assertion.
|
||||||
|
# C0103: invalid-name for maxDiff.
|
||||||
|
self.maxDiff = None # pylint: disable=C0103
|
||||||
|
|
||||||
|
self._EXPECTED_BOOT_SIGNATURE_RSA2048 = ( # pylint: disable=C0103
|
||||||
|
'Minimum libavb version: 1.0\n'
|
||||||
|
'Header Block: 256 bytes\n'
|
||||||
|
'Authentication Block: 320 bytes\n'
|
||||||
|
'Auxiliary Block: 832 bytes\n'
|
||||||
|
'Public key (sha1): '
|
||||||
|
'cdbb77177f731920bbe0a0f94f84d9038ae0617d\n'
|
||||||
|
'Algorithm: SHA256_RSA2048\n'
|
||||||
|
'Rollback Index: 0\n'
|
||||||
|
'Flags: 0\n'
|
||||||
|
'Rollback Index Location: 0\n'
|
||||||
|
"Release String: 'avbtool 1.2.0'\n"
|
||||||
|
'Descriptors:\n'
|
||||||
|
' Hash descriptor:\n'
|
||||||
|
' Image Size: 8192 bytes\n'
|
||||||
|
' Hash Algorithm: sha256\n'
|
||||||
|
' Partition Name: boot\n' # boot
|
||||||
|
' Salt: d00df00d\n'
|
||||||
|
' Digest: '
|
||||||
|
'faf1da72a4fba97ddab0b8f7a410db86'
|
||||||
|
'8fb72392a66d1440ff8bff490c73c771\n'
|
||||||
|
' Flags: 0\n'
|
||||||
|
" Prop: gki -> 'nice'\n"
|
||||||
|
" Prop: space -> 'nice to meet you'\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
self._EXPECTED_KERNEL_SIGNATURE_RSA2048 = ( # pylint: disable=C0103
|
||||||
|
'Minimum libavb version: 1.0\n'
|
||||||
|
'Header Block: 256 bytes\n'
|
||||||
|
'Authentication Block: 320 bytes\n'
|
||||||
|
'Auxiliary Block: 832 bytes\n'
|
||||||
|
'Public key (sha1): '
|
||||||
|
'cdbb77177f731920bbe0a0f94f84d9038ae0617d\n'
|
||||||
|
'Algorithm: SHA256_RSA2048\n'
|
||||||
|
'Rollback Index: 0\n'
|
||||||
|
'Flags: 0\n'
|
||||||
|
'Rollback Index Location: 0\n'
|
||||||
|
"Release String: 'avbtool 1.2.0'\n"
|
||||||
|
'Descriptors:\n'
|
||||||
|
' Hash descriptor:\n'
|
||||||
|
' Image Size: 4096 bytes\n'
|
||||||
|
' Hash Algorithm: sha256\n'
|
||||||
|
' Partition Name: generic_kernel\n' # generic_kernel
|
||||||
|
' Salt: d00df00d\n'
|
||||||
|
' Digest: '
|
||||||
|
'762c877f3af0d50a4a4fbc1385d5c7ce'
|
||||||
|
'52a1288db74b33b72217d93db6f2909f\n'
|
||||||
|
' Flags: 0\n'
|
||||||
|
" Prop: gki -> 'nice'\n"
|
||||||
|
" Prop: space -> 'nice to meet you'\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
self._EXPECTED_BOOT_SIGNATURE_RSA4096 = ( # pylint: disable=C0103
|
||||||
|
'Minimum libavb version: 1.0\n'
|
||||||
|
'Header Block: 256 bytes\n'
|
||||||
|
'Authentication Block: 576 bytes\n'
|
||||||
|
'Auxiliary Block: 1344 bytes\n'
|
||||||
|
'Public key (sha1): '
|
||||||
|
'2597c218aae470a130f61162feaae70afd97f011\n'
|
||||||
|
'Algorithm: SHA256_RSA4096\n' # RSA4096
|
||||||
|
'Rollback Index: 0\n'
|
||||||
|
'Flags: 0\n'
|
||||||
|
'Rollback Index Location: 0\n'
|
||||||
|
"Release String: 'avbtool 1.2.0'\n"
|
||||||
|
'Descriptors:\n'
|
||||||
|
' Hash descriptor:\n'
|
||||||
|
' Image Size: 8192 bytes\n'
|
||||||
|
' Hash Algorithm: sha256\n'
|
||||||
|
' Partition Name: boot\n' # boot
|
||||||
|
' Salt: d00df00d\n'
|
||||||
|
' Digest: '
|
||||||
|
'faf1da72a4fba97ddab0b8f7a410db86'
|
||||||
|
'8fb72392a66d1440ff8bff490c73c771\n'
|
||||||
|
' Flags: 0\n'
|
||||||
|
" Prop: gki -> 'nice'\n"
|
||||||
|
" Prop: space -> 'nice to meet you'\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
self._EXPECTED_KERNEL_SIGNATURE_RSA4096 = ( # pylint: disable=C0103
|
||||||
|
'Minimum libavb version: 1.0\n'
|
||||||
|
'Header Block: 256 bytes\n'
|
||||||
|
'Authentication Block: 576 bytes\n'
|
||||||
|
'Auxiliary Block: 1344 bytes\n'
|
||||||
|
'Public key (sha1): '
|
||||||
|
'2597c218aae470a130f61162feaae70afd97f011\n'
|
||||||
|
'Algorithm: SHA256_RSA4096\n' # RSA4096
|
||||||
|
'Rollback Index: 0\n'
|
||||||
|
'Flags: 0\n'
|
||||||
|
'Rollback Index Location: 0\n'
|
||||||
|
"Release String: 'avbtool 1.2.0'\n"
|
||||||
|
'Descriptors:\n'
|
||||||
|
' Hash descriptor:\n'
|
||||||
|
' Image Size: 4096 bytes\n'
|
||||||
|
' Hash Algorithm: sha256\n'
|
||||||
|
' Partition Name: generic_kernel\n' # generic_kernel
|
||||||
|
' Salt: d00df00d\n'
|
||||||
|
' Digest: '
|
||||||
|
'762c877f3af0d50a4a4fbc1385d5c7ce'
|
||||||
|
'52a1288db74b33b72217d93db6f2909f\n'
|
||||||
|
' Flags: 0\n'
|
||||||
|
" Prop: gki -> 'nice'\n"
|
||||||
|
" Prop: space -> 'nice to meet you'\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
self._EXPECTED_BOOT_1_0_SIGNATURE1_RSA4096 = ( # pylint: disable=C0103
|
||||||
|
'Minimum libavb version: 1.0\n'
|
||||||
|
'Header Block: 256 bytes\n'
|
||||||
|
'Authentication Block: 576 bytes\n'
|
||||||
|
'Auxiliary Block: 1600 bytes\n'
|
||||||
|
'Public key (sha1): '
|
||||||
|
'2597c218aae470a130f61162feaae70afd97f011\n'
|
||||||
|
'Algorithm: SHA256_RSA4096\n' # RSA4096
|
||||||
|
'Rollback Index: 0\n'
|
||||||
|
'Flags: 0\n'
|
||||||
|
'Rollback Index Location: 0\n'
|
||||||
|
"Release String: 'avbtool 1.2.0'\n"
|
||||||
|
'Descriptors:\n'
|
||||||
|
' Hash descriptor:\n'
|
||||||
|
' Image Size: 12288 bytes\n'
|
||||||
|
' Hash Algorithm: sha256\n'
|
||||||
|
' Partition Name: boot\n' # boot
|
||||||
|
' Salt: d00df00d\n'
|
||||||
|
' Digest: '
|
||||||
|
'88465e463bffb9f7dfc0c1f46d01bcf3'
|
||||||
|
'15f7693e19bd188a0ca1feca2ed7b9df\n'
|
||||||
|
' Flags: 0\n'
|
||||||
|
" Prop: gki -> 'nice'\n"
|
||||||
|
" Prop: space -> 'nice to meet you'\n"
|
||||||
|
" Prop: KERNEL_RELEASE -> '5.10.42-android13-0-00544-"
|
||||||
|
"ged21d463f856'\n"
|
||||||
|
" Prop: BRANCH -> 'android13-5.10-2022-05'\n"
|
||||||
|
" Prop: BUILD_NUMBER -> 'ab8295296'\n"
|
||||||
|
" Prop: SPACE -> 'nice to meet you'\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
self._EXPECTED_BOOT_1_0_SIGNATURE2_RSA4096 = ( # pylint: disable=C0103
|
||||||
|
'Minimum libavb version: 1.0\n'
|
||||||
|
'Header Block: 256 bytes\n'
|
||||||
|
'Authentication Block: 576 bytes\n'
|
||||||
|
'Auxiliary Block: 1600 bytes\n'
|
||||||
|
'Public key (sha1): '
|
||||||
|
'2597c218aae470a130f61162feaae70afd97f011\n'
|
||||||
|
'Algorithm: SHA256_RSA4096\n' # RSA4096
|
||||||
|
'Rollback Index: 0\n'
|
||||||
|
'Flags: 0\n'
|
||||||
|
'Rollback Index Location: 0\n'
|
||||||
|
"Release String: 'avbtool 1.2.0'\n"
|
||||||
|
'Descriptors:\n'
|
||||||
|
' Hash descriptor:\n'
|
||||||
|
' Image Size: 8192 bytes\n'
|
||||||
|
' Hash Algorithm: sha256\n'
|
||||||
|
' Partition Name: generic_kernel\n' # generic_kernel
|
||||||
|
' Salt: d00df00d\n'
|
||||||
|
' Digest: '
|
||||||
|
'14ac8d0d233e57a317acd05cd458f2bb'
|
||||||
|
'cc78725ef9f66c1b38e90697fb09d943\n'
|
||||||
|
' Flags: 0\n'
|
||||||
|
" Prop: gki -> 'nice'\n"
|
||||||
|
" Prop: space -> 'nice to meet you'\n"
|
||||||
|
" Prop: KERNEL_RELEASE -> '5.10.42-android13-0-00544-"
|
||||||
|
"ged21d463f856'\n"
|
||||||
|
" Prop: BRANCH -> 'android13-5.10-2022-05'\n"
|
||||||
|
" Prop: BUILD_NUMBER -> 'ab8295296'\n"
|
||||||
|
" Prop: SPACE -> 'nice to meet you'\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
self._EXPECTED_BOOT_2_0_SIGNATURE1_RSA4096 = ( # pylint: disable=C0103
|
||||||
|
'Minimum libavb version: 1.0\n'
|
||||||
|
'Header Block: 256 bytes\n'
|
||||||
|
'Authentication Block: 576 bytes\n'
|
||||||
|
'Auxiliary Block: 1600 bytes\n'
|
||||||
|
'Public key (sha1): '
|
||||||
|
'2597c218aae470a130f61162feaae70afd97f011\n'
|
||||||
|
'Algorithm: SHA256_RSA4096\n' # RSA4096
|
||||||
|
'Rollback Index: 0\n'
|
||||||
|
'Flags: 0\n'
|
||||||
|
'Rollback Index Location: 0\n'
|
||||||
|
"Release String: 'avbtool 1.2.0'\n"
|
||||||
|
'Descriptors:\n'
|
||||||
|
' Hash descriptor:\n'
|
||||||
|
' Image Size: 20480 bytes\n'
|
||||||
|
' Hash Algorithm: sha256\n'
|
||||||
|
' Partition Name: boot\n' # boot
|
||||||
|
' Salt: d00df00d\n'
|
||||||
|
' Digest: '
|
||||||
|
'3e6a9854a9d2350a7071083bc3f37376'
|
||||||
|
'37573fd87b1c72b146cb4870ac6af36f\n'
|
||||||
|
' Flags: 0\n'
|
||||||
|
" Prop: gki -> 'nice'\n"
|
||||||
|
" Prop: space -> 'nice to meet you'\n"
|
||||||
|
" Prop: KERNEL_RELEASE -> '5.10.42-android13-0-00544-"
|
||||||
|
"ged21d463f856'\n"
|
||||||
|
" Prop: BRANCH -> 'android13-5.10-2022-05'\n"
|
||||||
|
" Prop: BUILD_NUMBER -> 'ab8295296'\n"
|
||||||
|
" Prop: SPACE -> 'nice to meet you'\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
self._EXPECTED_BOOT_2_0_SIGNATURE2_RSA4096 = ( # pylint: disable=C0103
|
||||||
|
'Minimum libavb version: 1.0\n'
|
||||||
|
'Header Block: 256 bytes\n'
|
||||||
|
'Authentication Block: 576 bytes\n'
|
||||||
|
'Auxiliary Block: 1600 bytes\n'
|
||||||
|
'Public key (sha1): '
|
||||||
|
'2597c218aae470a130f61162feaae70afd97f011\n'
|
||||||
|
'Algorithm: SHA256_RSA4096\n' # RSA4096
|
||||||
|
'Rollback Index: 0\n'
|
||||||
|
'Flags: 0\n'
|
||||||
|
'Rollback Index Location: 0\n'
|
||||||
|
"Release String: 'avbtool 1.2.0'\n"
|
||||||
|
'Descriptors:\n'
|
||||||
|
' Hash descriptor:\n'
|
||||||
|
' Image Size: 16384 bytes\n'
|
||||||
|
' Hash Algorithm: sha256\n'
|
||||||
|
' Partition Name: generic_kernel\n' # generic_kernel
|
||||||
|
' Salt: d00df00d\n'
|
||||||
|
' Digest: '
|
||||||
|
'92fb8443cd284b67a4cbf5ce00348b50'
|
||||||
|
'1c657e0aedf4e2181c92ad7fc8b5224f\n'
|
||||||
|
' Flags: 0\n'
|
||||||
|
" Prop: gki -> 'nice'\n"
|
||||||
|
" Prop: space -> 'nice to meet you'\n"
|
||||||
|
" Prop: KERNEL_RELEASE -> '5.10.42-android13-0-00544-"
|
||||||
|
"ged21d463f856'\n"
|
||||||
|
" Prop: BRANCH -> 'android13-5.10-2022-05'\n"
|
||||||
|
" Prop: BUILD_NUMBER -> 'ab8295296'\n"
|
||||||
|
" Prop: SPACE -> 'nice to meet you'\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
def _test_boot_signatures(self, signatures_dir, expected_signatures_info):
|
||||||
|
"""Tests the info of each boot signature under the signature directory.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
signatures_dir: the directory containing the boot signatures. e.g.,
|
||||||
|
- signatures_dir/boot_signature1
|
||||||
|
- signatures_dir/boot_signature2
|
||||||
|
expected_signatures_info: A dict containing the expected output
|
||||||
|
of `avbtool info_image` for each signature under
|
||||||
|
|signatures_dir|. e.g.,
|
||||||
|
{'boot_signature1': expected_stdout_signature1
|
||||||
|
'boot_signature2': expected_stdout_signature2}
|
||||||
|
"""
|
||||||
|
for signature in expected_signatures_info:
|
||||||
|
avbtool_info_cmds = [
|
||||||
|
'avbtool', 'info_image', '--image',
|
||||||
|
os.path.join(signatures_dir, signature)
|
||||||
|
]
|
||||||
|
result = subprocess.run(avbtool_info_cmds, check=True,
|
||||||
|
capture_output=True, encoding='utf-8')
|
||||||
|
self.assertEqual(result.stdout, expected_signatures_info[signature])
|
||||||
|
|
||||||
|
def test_certify_bootimg_without_avb_footer(self):
|
||||||
|
"""Tests certify_bootimg on a boot image without an AVB footer."""
|
||||||
|
with tempfile.TemporaryDirectory() as temp_out_dir:
|
||||||
|
boot_img = os.path.join(temp_out_dir, 'boot.img')
|
||||||
|
generate_test_boot_image(boot_img)
|
||||||
|
|
||||||
|
# Generates the certified boot image, with a RSA2048 key.
|
||||||
|
boot_certified_img = os.path.join(temp_out_dir,
|
||||||
|
'boot-certified.img')
|
||||||
|
certify_bootimg_cmds = [
|
||||||
|
'certify_bootimg',
|
||||||
|
'--boot_img', boot_img,
|
||||||
|
'--algorithm', 'SHA256_RSA2048',
|
||||||
|
'--key', './testdata/testkey_rsa2048.pem',
|
||||||
|
'--extra_args', '--prop gki:nice '
|
||||||
|
'--prop space:"nice to meet you"',
|
||||||
|
'--output', boot_certified_img,
|
||||||
|
]
|
||||||
|
subprocess.run(certify_bootimg_cmds, check=True, cwd=self._exec_dir)
|
||||||
|
|
||||||
|
extract_boot_signatures(boot_certified_img, temp_out_dir)
|
||||||
|
self._test_boot_signatures(
|
||||||
|
temp_out_dir,
|
||||||
|
{'boot_signature1': self._EXPECTED_BOOT_SIGNATURE_RSA2048,
|
||||||
|
'boot_signature2': self._EXPECTED_KERNEL_SIGNATURE_RSA2048})
|
||||||
|
|
||||||
|
# Generates the certified boot image again, with a RSA4096 key.
|
||||||
|
boot_certified2_img = os.path.join(temp_out_dir,
|
||||||
|
'boot-certified2.img')
|
||||||
|
certify_bootimg_cmds = [
|
||||||
|
'certify_bootimg',
|
||||||
|
'--boot_img', boot_certified_img,
|
||||||
|
'--algorithm', 'SHA256_RSA4096',
|
||||||
|
'--key', './testdata/testkey_rsa4096.pem',
|
||||||
|
'--extra_args', '--prop gki:nice '
|
||||||
|
'--prop space:"nice to meet you"',
|
||||||
|
'--output', boot_certified2_img,
|
||||||
|
]
|
||||||
|
subprocess.run(certify_bootimg_cmds, check=True, cwd=self._exec_dir)
|
||||||
|
|
||||||
|
extract_boot_signatures(boot_certified2_img, temp_out_dir)
|
||||||
|
self._test_boot_signatures(
|
||||||
|
temp_out_dir,
|
||||||
|
{'boot_signature1': self._EXPECTED_BOOT_SIGNATURE_RSA4096,
|
||||||
|
'boot_signature2': self._EXPECTED_KERNEL_SIGNATURE_RSA4096})
|
||||||
|
|
||||||
|
def test_certify_bootimg_with_avb_footer(self):
|
||||||
|
"""Tests the AVB footer location remains after certify_bootimg."""
|
||||||
|
with tempfile.TemporaryDirectory() as temp_out_dir:
|
||||||
|
boot_img = os.path.join(temp_out_dir, 'boot.img')
|
||||||
|
generate_test_boot_image(boot_img=boot_img,
|
||||||
|
avb_partition_size=128 * 1024)
|
||||||
|
self.assertTrue(has_avb_footer(boot_img))
|
||||||
|
|
||||||
|
# Generates the certified boot image, with a RSA2048 key.
|
||||||
|
boot_certified_img = os.path.join(temp_out_dir,
|
||||||
|
'boot-certified.img')
|
||||||
|
certify_bootimg_cmds = [
|
||||||
|
'certify_bootimg',
|
||||||
|
'--boot_img', boot_img,
|
||||||
|
'--algorithm', 'SHA256_RSA2048',
|
||||||
|
'--key', './testdata/testkey_rsa2048.pem',
|
||||||
|
'--extra_args', '--prop gki:nice '
|
||||||
|
'--prop space:"nice to meet you"',
|
||||||
|
'--output', boot_certified_img,
|
||||||
|
]
|
||||||
|
subprocess.run(certify_bootimg_cmds, check=True, cwd=self._exec_dir)
|
||||||
|
|
||||||
|
# Checks an AVB footer exists and the image size remains.
|
||||||
|
self.assertTrue(has_avb_footer(boot_certified_img))
|
||||||
|
self.assertEqual(os.path.getsize(boot_img),
|
||||||
|
os.path.getsize(boot_certified_img))
|
||||||
|
|
||||||
|
extract_boot_signatures(boot_certified_img, temp_out_dir)
|
||||||
|
self._test_boot_signatures(
|
||||||
|
temp_out_dir,
|
||||||
|
{'boot_signature1': self._EXPECTED_BOOT_SIGNATURE_RSA2048,
|
||||||
|
'boot_signature2': self._EXPECTED_KERNEL_SIGNATURE_RSA2048})
|
||||||
|
|
||||||
|
# Generates the certified boot image again, with a RSA4096 key.
|
||||||
|
boot_certified2_img = os.path.join(temp_out_dir,
|
||||||
|
'boot-certified2.img')
|
||||||
|
certify_bootimg_cmds = [
|
||||||
|
'certify_bootimg',
|
||||||
|
'--boot_img', boot_certified_img,
|
||||||
|
'--algorithm', 'SHA256_RSA4096',
|
||||||
|
'--key', './testdata/testkey_rsa4096.pem',
|
||||||
|
'--extra_args', '--prop gki:nice '
|
||||||
|
'--prop space:"nice to meet you"',
|
||||||
|
'--output', boot_certified2_img,
|
||||||
|
]
|
||||||
|
subprocess.run(certify_bootimg_cmds, check=True, cwd=self._exec_dir)
|
||||||
|
|
||||||
|
# Checks an AVB footer exists and the image size remains.
|
||||||
|
self.assertTrue(has_avb_footer(boot_certified2_img))
|
||||||
|
self.assertEqual(os.path.getsize(boot_certified_img),
|
||||||
|
os.path.getsize(boot_certified2_img))
|
||||||
|
|
||||||
|
extract_boot_signatures(boot_certified2_img, temp_out_dir)
|
||||||
|
self._test_boot_signatures(
|
||||||
|
temp_out_dir,
|
||||||
|
{'boot_signature1': self._EXPECTED_BOOT_SIGNATURE_RSA4096,
|
||||||
|
'boot_signature2': self._EXPECTED_KERNEL_SIGNATURE_RSA4096})
|
||||||
|
|
||||||
|
def test_certify_bootimg_exceed_size(self):
|
||||||
|
"""Tests the boot signature size exceeded max size of the signature."""
|
||||||
|
with tempfile.TemporaryDirectory() as temp_out_dir:
|
||||||
|
boot_img = os.path.join(temp_out_dir, 'boot.img')
|
||||||
|
generate_test_boot_image(boot_img)
|
||||||
|
|
||||||
|
# Certifies the boot.img with many --extra_args, and checks
|
||||||
|
# it will raise the ValueError() exception.
|
||||||
|
boot_certified_img = os.path.join(temp_out_dir,
|
||||||
|
'boot-certified.img')
|
||||||
|
certify_bootimg_cmds = [
|
||||||
|
'certify_bootimg',
|
||||||
|
'--boot_img', boot_img,
|
||||||
|
'--algorithm', 'SHA256_RSA2048',
|
||||||
|
'--key', './testdata/testkey_rsa2048.pem',
|
||||||
|
# Makes it exceed the signature max size.
|
||||||
|
'--extra_args', '--prop foo:bar --prop gki:nice ' * 128,
|
||||||
|
'--output', boot_certified_img,
|
||||||
|
]
|
||||||
|
|
||||||
|
try:
|
||||||
|
subprocess.run(certify_bootimg_cmds, check=True,
|
||||||
|
capture_output=True, cwd=self._exec_dir,
|
||||||
|
encoding='utf-8')
|
||||||
|
self.fail('Exceeding signature size assertion is not raised')
|
||||||
|
except subprocess.CalledProcessError as err:
|
||||||
|
self.assertIn('ValueError: boot_signature size must be <= ',
|
||||||
|
err.stderr)
|
||||||
|
|
||||||
|
def test_certify_bootimg_archive(self):
|
||||||
|
"""Tests certify_bootimg for a boot-img.zip."""
|
||||||
|
with tempfile.TemporaryDirectory() as temp_out_dir:
|
||||||
|
boot_img_zip = os.path.join(temp_out_dir, 'boot-img.zip')
|
||||||
|
generate_test_boot_image_archive(
|
||||||
|
boot_img_zip,
|
||||||
|
# A list of (boot_img_name, kernel_size, partition_size).
|
||||||
|
[('boot-1.0.img', 8 * 1024, 128 * 1024),
|
||||||
|
('boot-2.0.img', 16 * 1024, 256 * 1024)])
|
||||||
|
|
||||||
|
# Certify the boot image archive, with a RSA4096 key.
|
||||||
|
boot_certified_img_zip = os.path.join(temp_out_dir,
|
||||||
|
'boot-certified-img.zip')
|
||||||
|
certify_bootimg_cmds = [
|
||||||
|
'certify_bootimg',
|
||||||
|
'--boot_img_zip', boot_img_zip,
|
||||||
|
'--algorithm', 'SHA256_RSA4096',
|
||||||
|
'--key', './testdata/testkey_rsa4096.pem',
|
||||||
|
'--extra_args', '--prop gki:nice '
|
||||||
|
'--prop space:"nice to meet you"',
|
||||||
|
'--output', boot_certified_img_zip,
|
||||||
|
]
|
||||||
|
subprocess.run(certify_bootimg_cmds, check=True, cwd=self._exec_dir)
|
||||||
|
|
||||||
|
extract_boot_archive_with_signatures(boot_certified_img_zip,
|
||||||
|
temp_out_dir)
|
||||||
|
|
||||||
|
# Checks an AVB footer exists and the image size remains.
|
||||||
|
boot_1_img = os.path.join(temp_out_dir, 'boot-1.0.img')
|
||||||
|
self.assertTrue(has_avb_footer(boot_1_img))
|
||||||
|
self.assertEqual(os.path.getsize(boot_1_img), 128 * 1024)
|
||||||
|
|
||||||
|
boot_2_img = os.path.join(temp_out_dir, 'boot-2.0.img')
|
||||||
|
self.assertTrue(has_avb_footer(boot_2_img))
|
||||||
|
self.assertEqual(os.path.getsize(boot_2_img), 256 * 1024)
|
||||||
|
|
||||||
|
self._test_boot_signatures(
|
||||||
|
temp_out_dir,
|
||||||
|
{'boot-1.0/boot_signature1':
|
||||||
|
self._EXPECTED_BOOT_1_0_SIGNATURE1_RSA4096,
|
||||||
|
'boot-1.0/boot_signature2':
|
||||||
|
self._EXPECTED_BOOT_1_0_SIGNATURE2_RSA4096,
|
||||||
|
'boot-2.0/boot_signature1':
|
||||||
|
self._EXPECTED_BOOT_2_0_SIGNATURE1_RSA4096,
|
||||||
|
'boot-2.0/boot_signature2':
|
||||||
|
self._EXPECTED_BOOT_2_0_SIGNATURE2_RSA4096})
|
||||||
|
|
||||||
|
|
||||||
|
# I don't know how, but we need both the logger configuration and verbosity
|
||||||
|
# level > 2 to make atest work. And yes this line needs to be at the very top
|
||||||
|
# level, not even in the "__main__" indentation block.
|
||||||
|
logging.basicConfig(stream=sys.stdout)
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
unittest.main(verbosity=2)
|
||||||
@ -0,0 +1,94 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
#
|
||||||
|
# Copyright 2021, The Android Open Source Project
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
#
|
||||||
|
|
||||||
|
"""Generate a Generic Boot Image certificate suitable for VTS verification."""
|
||||||
|
|
||||||
|
from argparse import ArgumentParser
|
||||||
|
import shlex
|
||||||
|
import subprocess
|
||||||
|
|
||||||
|
|
||||||
|
def generate_gki_certificate(image, avbtool, name, algorithm, key, salt,
|
||||||
|
additional_avb_args, output):
|
||||||
|
"""Shell out to avbtool to generate a GKI certificate."""
|
||||||
|
|
||||||
|
# Need to specify a value of --partition_size for avbtool to work.
|
||||||
|
# We use 64 MB below, but avbtool will not resize the boot image to
|
||||||
|
# this size because --do_not_append_vbmeta_image is also specified.
|
||||||
|
avbtool_cmd = [
|
||||||
|
avbtool, 'add_hash_footer',
|
||||||
|
'--partition_name', name,
|
||||||
|
'--partition_size', str(64 * 1024 * 1024),
|
||||||
|
'--image', image,
|
||||||
|
'--algorithm', algorithm,
|
||||||
|
'--key', key,
|
||||||
|
'--do_not_append_vbmeta_image',
|
||||||
|
'--output_vbmeta_image', output,
|
||||||
|
]
|
||||||
|
|
||||||
|
if salt is not None:
|
||||||
|
avbtool_cmd += ['--salt', salt]
|
||||||
|
|
||||||
|
avbtool_cmd += additional_avb_args
|
||||||
|
|
||||||
|
subprocess.check_call(avbtool_cmd)
|
||||||
|
|
||||||
|
|
||||||
|
def parse_cmdline():
|
||||||
|
parser = ArgumentParser(add_help=True)
|
||||||
|
|
||||||
|
# Required args.
|
||||||
|
parser.add_argument('image', help='path to the image')
|
||||||
|
parser.add_argument('-o', '--output', required=True,
|
||||||
|
help='output certificate file name')
|
||||||
|
parser.add_argument('--name', required=True,
|
||||||
|
choices=['boot', 'generic_kernel'],
|
||||||
|
help='name of the image to be certified')
|
||||||
|
parser.add_argument('--algorithm', required=True,
|
||||||
|
help='AVB signing algorithm')
|
||||||
|
parser.add_argument('--key', required=True,
|
||||||
|
help='path to the RSA private key')
|
||||||
|
|
||||||
|
# Optional args.
|
||||||
|
parser.add_argument('--avbtool', default='avbtool',
|
||||||
|
help='path to the avbtool executable')
|
||||||
|
parser.add_argument('--salt', help='salt to use when computing image hash')
|
||||||
|
parser.add_argument('--additional_avb_args', default=[], action='append',
|
||||||
|
help='additional arguments to be forwarded to avbtool')
|
||||||
|
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
additional_avb_args = []
|
||||||
|
for a in args.additional_avb_args:
|
||||||
|
additional_avb_args.extend(shlex.split(a))
|
||||||
|
args.additional_avb_args = additional_avb_args
|
||||||
|
|
||||||
|
return args
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
args = parse_cmdline()
|
||||||
|
generate_gki_certificate(
|
||||||
|
image=args.image, avbtool=args.avbtool, name=args.name,
|
||||||
|
algorithm=args.algorithm, key=args.key, salt=args.salt,
|
||||||
|
additional_avb_args=args.additional_avb_args,
|
||||||
|
output=args.output,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
@ -0,0 +1,231 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
#
|
||||||
|
# Copyright (C) 2022 The Android Open Source Project
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
#
|
||||||
|
|
||||||
|
#
|
||||||
|
# Retrofits GKI boot images for upgrading devices.
|
||||||
|
#
|
||||||
|
|
||||||
|
set -eo errtrace
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
cat <<EOF
|
||||||
|
Usage:
|
||||||
|
$0 --boot BOOT --init_boot INIT_BOOT --version {3,4} -o OUTPUT
|
||||||
|
$0 --boot BOOT --init_boot INIT_BOOT --vendor_boot VENDOR_BOOT --version 2 -o OUTPUT
|
||||||
|
|
||||||
|
Options:
|
||||||
|
--boot FILE
|
||||||
|
Path to the generic boot image.
|
||||||
|
--init_boot FILE
|
||||||
|
Path to the generic init_boot image.
|
||||||
|
--vendor_boot FILE
|
||||||
|
Path to the vendor boot image.
|
||||||
|
--version {2,3,4}
|
||||||
|
Boot image header version to retrofit to.
|
||||||
|
-o, --output FILE
|
||||||
|
Path to the output boot image.
|
||||||
|
-v, --verbose
|
||||||
|
Show debug messages.
|
||||||
|
-h, --help, --usage
|
||||||
|
Show this help message.
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
die() {
|
||||||
|
echo >&2 "ERROR:" "${@}"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
file_size() {
|
||||||
|
stat -c '%s' "$1"
|
||||||
|
}
|
||||||
|
|
||||||
|
get_arg() {
|
||||||
|
local arg="$1"
|
||||||
|
shift
|
||||||
|
while [[ "$#" -gt 0 ]]; do
|
||||||
|
if [[ "$1" == "${arg}" ]]; then
|
||||||
|
shift
|
||||||
|
echo "$1"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
shift
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
TEMP_DIR="$(mktemp -d --tmpdir retrofit_gki.XXXXXXXX)"
|
||||||
|
readonly TEMP_DIR
|
||||||
|
|
||||||
|
exit_handler() {
|
||||||
|
readonly EXIT_CODE="$?"
|
||||||
|
rm -rf "${TEMP_DIR}" ||:
|
||||||
|
exit "${EXIT_CODE}"
|
||||||
|
}
|
||||||
|
|
||||||
|
trap exit_handler EXIT
|
||||||
|
trap 'die "line ${LINENO}, ${FUNCNAME:-<main>}(): \"${BASH_COMMAND}\" returned \"$?\"" ' ERR
|
||||||
|
|
||||||
|
while [[ "$1" =~ ^- ]]; do
|
||||||
|
case "$1" in
|
||||||
|
--boot )
|
||||||
|
shift
|
||||||
|
BOOT_IMAGE="$1"
|
||||||
|
;;
|
||||||
|
--init_boot )
|
||||||
|
shift
|
||||||
|
INIT_BOOT_IMAGE="$1"
|
||||||
|
;;
|
||||||
|
--vendor_boot )
|
||||||
|
shift
|
||||||
|
VENDOR_BOOT_IMAGE="$1"
|
||||||
|
;;
|
||||||
|
--version )
|
||||||
|
shift
|
||||||
|
OUTPUT_BOOT_IMAGE_VERSION="$1"
|
||||||
|
;;
|
||||||
|
-o | --output )
|
||||||
|
shift
|
||||||
|
OUTPUT_BOOT_IMAGE="$1"
|
||||||
|
;;
|
||||||
|
-v | --verbose )
|
||||||
|
VERBOSE=true
|
||||||
|
;;
|
||||||
|
-- )
|
||||||
|
shift
|
||||||
|
break
|
||||||
|
;;
|
||||||
|
-h | --help | --usage )
|
||||||
|
usage
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
* )
|
||||||
|
echo >&2 "Unexpected flag: '$1'"
|
||||||
|
usage >&2
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
shift
|
||||||
|
done
|
||||||
|
|
||||||
|
declare -ir OUTPUT_BOOT_IMAGE_VERSION
|
||||||
|
readonly BOOT_IMAGE
|
||||||
|
readonly INIT_BOOT_IMAGE
|
||||||
|
readonly VENDOR_BOOT_IMAGE
|
||||||
|
readonly OUTPUT_BOOT_IMAGE
|
||||||
|
readonly VERBOSE
|
||||||
|
|
||||||
|
# Make sure the input arguments make sense.
|
||||||
|
[[ -f "${BOOT_IMAGE}" ]] ||
|
||||||
|
die "argument '--boot': not a regular file: '${BOOT_IMAGE}'"
|
||||||
|
[[ -f "${INIT_BOOT_IMAGE}" ]] ||
|
||||||
|
die "argument '--init_boot': not a regular file: '${INIT_BOOT_IMAGE}'"
|
||||||
|
if [[ "${OUTPUT_BOOT_IMAGE_VERSION}" -lt 2 ]] || [[ "${OUTPUT_BOOT_IMAGE_VERSION}" -gt 4 ]]; then
|
||||||
|
die "argument '--version': valid choices are {2, 3, 4}"
|
||||||
|
elif [[ "${OUTPUT_BOOT_IMAGE_VERSION}" -eq 2 ]]; then
|
||||||
|
[[ -f "${VENDOR_BOOT_IMAGE}" ]] ||
|
||||||
|
die "argument '--vendor_boot': not a regular file: '${VENDOR_BOOT_IMAGE}'"
|
||||||
|
fi
|
||||||
|
[[ -z "${OUTPUT_BOOT_IMAGE}" ]] &&
|
||||||
|
die "argument '--output': cannot be empty"
|
||||||
|
|
||||||
|
readonly BOOT_IMAGE_WITHOUT_AVB_FOOTER="${TEMP_DIR}/boot.img.without_avb_footer"
|
||||||
|
readonly BOOT_DIR="${TEMP_DIR}/boot"
|
||||||
|
readonly INIT_BOOT_DIR="${TEMP_DIR}/init_boot"
|
||||||
|
readonly VENDOR_BOOT_DIR="${TEMP_DIR}/vendor_boot"
|
||||||
|
readonly VENDOR_BOOT_MKBOOTIMG_ARGS="${TEMP_DIR}/vendor_boot.mkbootimg_args"
|
||||||
|
readonly OUTPUT_RAMDISK="${TEMP_DIR}/out.ramdisk"
|
||||||
|
readonly OUTPUT_BOOT_SIGNATURE="${TEMP_DIR}/out.boot_signature"
|
||||||
|
|
||||||
|
readonly AVBTOOL="${AVBTOOL:-avbtool}"
|
||||||
|
readonly MKBOOTIMG="${MKBOOTIMG:-mkbootimg}"
|
||||||
|
readonly UNPACK_BOOTIMG="${UNPACK_BOOTIMG:-unpack_bootimg}"
|
||||||
|
|
||||||
|
# Fixed boot signature size for easy discovery in VTS.
|
||||||
|
readonly BOOT_SIGNATURE_SIZE=$(( 16 << 10 ))
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# Preparations are done. Now begin the actual work.
|
||||||
|
#
|
||||||
|
|
||||||
|
# Copy the boot image because `avbtool erase_footer` edits the file in-place.
|
||||||
|
cp "${BOOT_IMAGE}" "${BOOT_IMAGE_WITHOUT_AVB_FOOTER}"
|
||||||
|
( [[ -n "${VERBOSE}" ]] && set -x
|
||||||
|
"${AVBTOOL}" erase_footer --image "${BOOT_IMAGE_WITHOUT_AVB_FOOTER}" 2>/dev/null ||:
|
||||||
|
tail -c "${BOOT_SIGNATURE_SIZE}" "${BOOT_IMAGE_WITHOUT_AVB_FOOTER}" > "${OUTPUT_BOOT_SIGNATURE}"
|
||||||
|
"${UNPACK_BOOTIMG}" --boot_img "${BOOT_IMAGE}" --out "${BOOT_DIR}" >/dev/null
|
||||||
|
"${UNPACK_BOOTIMG}" --boot_img "${INIT_BOOT_IMAGE}" --out "${INIT_BOOT_DIR}" >/dev/null
|
||||||
|
)
|
||||||
|
if [[ "$(file_size "${OUTPUT_BOOT_SIGNATURE}")" -ne "${BOOT_SIGNATURE_SIZE}" ]]; then
|
||||||
|
die "boot signature size must be equal to ${BOOT_SIGNATURE_SIZE}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
declare -a mkbootimg_args=()
|
||||||
|
|
||||||
|
if [[ "${OUTPUT_BOOT_IMAGE_VERSION}" -eq 4 ]]; then
|
||||||
|
mkbootimg_args+=( \
|
||||||
|
--header_version 4 \
|
||||||
|
--kernel "${BOOT_DIR}/kernel" \
|
||||||
|
--ramdisk "${INIT_BOOT_DIR}/ramdisk" \
|
||||||
|
)
|
||||||
|
elif [[ "${OUTPUT_BOOT_IMAGE_VERSION}" -eq 3 ]]; then
|
||||||
|
mkbootimg_args+=( \
|
||||||
|
--header_version 3 \
|
||||||
|
--kernel "${BOOT_DIR}/kernel" \
|
||||||
|
--ramdisk "${INIT_BOOT_DIR}/ramdisk" \
|
||||||
|
)
|
||||||
|
elif [[ "${OUTPUT_BOOT_IMAGE_VERSION}" -eq 2 ]]; then
|
||||||
|
( [[ -n "${VERBOSE}" ]] && set -x
|
||||||
|
"${UNPACK_BOOTIMG}" --boot_img "${VENDOR_BOOT_IMAGE}" --out "${VENDOR_BOOT_DIR}" \
|
||||||
|
--format=mkbootimg -0 > "${VENDOR_BOOT_MKBOOTIMG_ARGS}"
|
||||||
|
cat "${VENDOR_BOOT_DIR}/vendor_ramdisk" "${INIT_BOOT_DIR}/ramdisk" > "${OUTPUT_RAMDISK}"
|
||||||
|
)
|
||||||
|
|
||||||
|
declare -a vendor_boot_args=()
|
||||||
|
while IFS= read -r -d '' ARG; do
|
||||||
|
vendor_boot_args+=("${ARG}")
|
||||||
|
done < "${VENDOR_BOOT_MKBOOTIMG_ARGS}"
|
||||||
|
|
||||||
|
pagesize="$(get_arg --pagesize "${vendor_boot_args[@]}")"
|
||||||
|
kernel_offset="$(get_arg --kernel_offset "${vendor_boot_args[@]}")"
|
||||||
|
ramdisk_offset="$(get_arg --ramdisk_offset "${vendor_boot_args[@]}")"
|
||||||
|
tags_offset="$(get_arg --tags_offset "${vendor_boot_args[@]}")"
|
||||||
|
dtb_offset="$(get_arg --dtb_offset "${vendor_boot_args[@]}")"
|
||||||
|
kernel_cmdline="$(get_arg --vendor_cmdline "${vendor_boot_args[@]}")"
|
||||||
|
|
||||||
|
mkbootimg_args+=( \
|
||||||
|
--header_version 2 \
|
||||||
|
--base 0 \
|
||||||
|
--kernel_offset "${kernel_offset}" \
|
||||||
|
--ramdisk_offset "${ramdisk_offset}" \
|
||||||
|
--second_offset 0 \
|
||||||
|
--tags_offset "${tags_offset}" \
|
||||||
|
--dtb_offset "${dtb_offset}" \
|
||||||
|
--cmdline "${kernel_cmdline}" \
|
||||||
|
--pagesize "${pagesize}" \
|
||||||
|
--kernel "${BOOT_DIR}/kernel" \
|
||||||
|
--ramdisk "${OUTPUT_RAMDISK}" \
|
||||||
|
)
|
||||||
|
if [[ -f "${VENDOR_BOOT_DIR}/dtb" ]]; then
|
||||||
|
mkbootimg_args+=(--dtb "${VENDOR_BOOT_DIR}/dtb")
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
( [[ -n "${VERBOSE}" ]] && set -x
|
||||||
|
"${MKBOOTIMG}" "${mkbootimg_args[@]}" --output "${OUTPUT_BOOT_IMAGE}"
|
||||||
|
cat "${OUTPUT_BOOT_SIGNATURE}" >> "${OUTPUT_BOOT_IMAGE}"
|
||||||
|
)
|
||||||
@ -0,0 +1,144 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
#
|
||||||
|
# Copyright (C) 2022 The Android Open Source Project
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
#
|
||||||
|
|
||||||
|
set -eo errtrace
|
||||||
|
|
||||||
|
die() {
|
||||||
|
echo >&2 "ERROR:" "${@}"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
trap 'die "line ${LINENO}, ${FUNCNAME:-<main>}(): \"${BASH_COMMAND}\" returned \"$?\"" ' ERR
|
||||||
|
|
||||||
|
# Figure out where we are and where to look for test executables.
|
||||||
|
cd "$(dirname "${BASH_SOURCE[0]}")"
|
||||||
|
TEST_DIR="$(pwd)"
|
||||||
|
readonly TEST_DIR
|
||||||
|
readonly TEMP_DIR="${TEST_DIR}/stage.retrofit_gki_test"
|
||||||
|
|
||||||
|
export PATH="${TEST_DIR}:${PATH}"
|
||||||
|
rm -rf "${TEMP_DIR}"
|
||||||
|
mkdir -p "${TEMP_DIR}"
|
||||||
|
|
||||||
|
# Generate some test files.
|
||||||
|
readonly TEST_DTB="${TEMP_DIR}/dtb"
|
||||||
|
readonly TEST_KERNEL="${TEMP_DIR}/kernel"
|
||||||
|
readonly TEST_RAMDISK="${TEMP_DIR}/ramdisk"
|
||||||
|
readonly TEST_VENDOR_RAMDISK="${TEMP_DIR}/vendor_ramdisk"
|
||||||
|
readonly TEST_BOOT_SIGNATURE="${TEMP_DIR}/boot.boot_signature"
|
||||||
|
readonly TEST_V2_RETROFITTED_RAMDISK="${TEMP_DIR}/retrofitted.ramdisk"
|
||||||
|
readonly TEST_BOOT_IMAGE="${TEMP_DIR}/boot.img"
|
||||||
|
readonly TEST_INIT_BOOT_IMAGE="${TEMP_DIR}/init_boot.img"
|
||||||
|
readonly TEST_VENDOR_BOOT_IMAGE="${TEMP_DIR}/vendor_boot.img"
|
||||||
|
|
||||||
|
( # Run these in subshell because dd is noisy.
|
||||||
|
dd if=/dev/urandom of="${TEST_DTB}" bs=1024 count=10
|
||||||
|
dd if=/dev/urandom of="${TEST_KERNEL}" bs=1024 count=10
|
||||||
|
dd if=/dev/urandom of="${TEST_RAMDISK}" bs=1024 count=10
|
||||||
|
dd if=/dev/urandom of="${TEST_VENDOR_RAMDISK}" bs=1024 count=10
|
||||||
|
dd if=/dev/urandom of="${TEST_BOOT_SIGNATURE}" bs=1024 count=16
|
||||||
|
) 2> /dev/null
|
||||||
|
|
||||||
|
cat "${TEST_VENDOR_RAMDISK}" "${TEST_RAMDISK}" > "${TEST_V2_RETROFITTED_RAMDISK}"
|
||||||
|
|
||||||
|
mkbootimg \
|
||||||
|
--header_version 4 \
|
||||||
|
--kernel "${TEST_KERNEL}" \
|
||||||
|
--output "${TEST_BOOT_IMAGE}"
|
||||||
|
cat "${TEST_BOOT_SIGNATURE}" >> "${TEST_BOOT_IMAGE}"
|
||||||
|
avbtool add_hash_footer --image "${TEST_BOOT_IMAGE}" --partition_name boot --partition_size $((20 << 20))
|
||||||
|
|
||||||
|
mkbootimg \
|
||||||
|
--header_version 4 \
|
||||||
|
--ramdisk "${TEST_RAMDISK}" \
|
||||||
|
--output "${TEST_INIT_BOOT_IMAGE}"
|
||||||
|
mkbootimg \
|
||||||
|
--header_version 4 \
|
||||||
|
--pagesize 4096 \
|
||||||
|
--dtb "${TEST_DTB}" \
|
||||||
|
--vendor_ramdisk "${TEST_VENDOR_RAMDISK}" \
|
||||||
|
--vendor_boot "${TEST_VENDOR_BOOT_IMAGE}"
|
||||||
|
|
||||||
|
readonly RETROFITTED_IMAGE="${TEMP_DIR}/retrofitted_boot.img"
|
||||||
|
readonly RETROFITTED_IMAGE_DIR="${TEMP_DIR}/retrofitted_boot.img.unpack"
|
||||||
|
readonly BOOT_SIGNATURE_SIZE=$(( 16 << 10 ))
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# Begin test.
|
||||||
|
#
|
||||||
|
echo >&2 "TEST: retrofit to boot v4"
|
||||||
|
|
||||||
|
retrofit_gki.sh \
|
||||||
|
--boot "${TEST_BOOT_IMAGE}" \
|
||||||
|
--init_boot "${TEST_INIT_BOOT_IMAGE}" \
|
||||||
|
--version 4 \
|
||||||
|
--output "${RETROFITTED_IMAGE}"
|
||||||
|
|
||||||
|
rm -rf "${RETROFITTED_IMAGE_DIR}"
|
||||||
|
unpack_bootimg --boot_img "${RETROFITTED_IMAGE}" --out "${RETROFITTED_IMAGE_DIR}" > /dev/null
|
||||||
|
tail -c "${BOOT_SIGNATURE_SIZE}" "${RETROFITTED_IMAGE}" > "${RETROFITTED_IMAGE_DIR}/boot_signature"
|
||||||
|
|
||||||
|
cmp -s "${TEST_KERNEL}" "${RETROFITTED_IMAGE_DIR}/kernel" ||
|
||||||
|
die "unexpected diff: kernel"
|
||||||
|
cmp -s "${TEST_RAMDISK}" "${RETROFITTED_IMAGE_DIR}/ramdisk" ||
|
||||||
|
die "unexpected diff: ramdisk"
|
||||||
|
cmp -s "${TEST_BOOT_SIGNATURE}" "${RETROFITTED_IMAGE_DIR}/boot_signature" ||
|
||||||
|
die "unexpected diff: boot signature"
|
||||||
|
|
||||||
|
|
||||||
|
echo >&2 "TEST: retrofit to boot v3"
|
||||||
|
|
||||||
|
retrofit_gki.sh \
|
||||||
|
--boot "${TEST_BOOT_IMAGE}" \
|
||||||
|
--init_boot "${TEST_INIT_BOOT_IMAGE}" \
|
||||||
|
--version 3 \
|
||||||
|
--output "${RETROFITTED_IMAGE}"
|
||||||
|
|
||||||
|
rm -rf "${RETROFITTED_IMAGE_DIR}"
|
||||||
|
unpack_bootimg --boot_img "${RETROFITTED_IMAGE}" --out "${RETROFITTED_IMAGE_DIR}" > /dev/null
|
||||||
|
tail -c "${BOOT_SIGNATURE_SIZE}" "${RETROFITTED_IMAGE}" > "${RETROFITTED_IMAGE_DIR}/boot_signature"
|
||||||
|
|
||||||
|
cmp -s "${TEST_KERNEL}" "${RETROFITTED_IMAGE_DIR}/kernel" ||
|
||||||
|
die "unexpected diff: kernel"
|
||||||
|
cmp -s "${TEST_RAMDISK}" "${RETROFITTED_IMAGE_DIR}/ramdisk" ||
|
||||||
|
die "unexpected diff: ramdisk"
|
||||||
|
cmp -s "${TEST_BOOT_SIGNATURE}" "${RETROFITTED_IMAGE_DIR}/boot_signature" ||
|
||||||
|
die "unexpected diff: boot signature"
|
||||||
|
|
||||||
|
|
||||||
|
echo >&2 "TEST: retrofit to boot v2"
|
||||||
|
|
||||||
|
retrofit_gki.sh \
|
||||||
|
--boot "${TEST_BOOT_IMAGE}" \
|
||||||
|
--init_boot "${TEST_INIT_BOOT_IMAGE}" \
|
||||||
|
--vendor_boot "${TEST_VENDOR_BOOT_IMAGE}" \
|
||||||
|
--version 2 \
|
||||||
|
--output "${RETROFITTED_IMAGE}"
|
||||||
|
|
||||||
|
rm -rf "${RETROFITTED_IMAGE_DIR}"
|
||||||
|
unpack_bootimg --boot_img "${RETROFITTED_IMAGE}" --out "${RETROFITTED_IMAGE_DIR}" > /dev/null
|
||||||
|
tail -c "${BOOT_SIGNATURE_SIZE}" "${RETROFITTED_IMAGE}" > "${RETROFITTED_IMAGE_DIR}/boot_signature"
|
||||||
|
|
||||||
|
cmp -s "${TEST_DTB}" "${RETROFITTED_IMAGE_DIR}/dtb" ||
|
||||||
|
die "unexpected diff: dtb"
|
||||||
|
cmp -s "${TEST_KERNEL}" "${RETROFITTED_IMAGE_DIR}/kernel" ||
|
||||||
|
die "unexpected diff: kernel"
|
||||||
|
cmp -s "${TEST_V2_RETROFITTED_RAMDISK}" "${RETROFITTED_IMAGE_DIR}/ramdisk" ||
|
||||||
|
die "unexpected diff: ramdisk"
|
||||||
|
cmp -s "${TEST_BOOT_SIGNATURE}" "${RETROFITTED_IMAGE_DIR}/boot_signature" ||
|
||||||
|
die "unexpected diff: boot signature"
|
||||||
@ -0,0 +1,27 @@
|
|||||||
|
-----BEGIN RSA PRIVATE KEY-----
|
||||||
|
MIIEowIBAAKCAQEAxlVR3TIkouAOvH79vaJTgFhpfvVKQIeVkFRZPVXK/zY0Gvrh
|
||||||
|
4JAqGjJoW/PfrQv5sdD36qtHH3a+G5hLZ6Ni+t/mtfjucxZfuLGC3kmJ1T3XqEKZ
|
||||||
|
gXXI2IR7vVSoImREvDQGEDyJwtHzLANlkbGg0cghVhWZSCAndO8BenalC2v94/rt
|
||||||
|
DfkPekH6dgU3Sf40T0sBSeSY94mOzTaqOR2pfV1rWlLRdWmo33zeHBv52Rlbt0dM
|
||||||
|
uXAureXWiHztkm5GCBC1dgM+CaxNtizNEgC91KcD0xuRCCM2WxH+r1lpszyIJDct
|
||||||
|
YbrFmVEYl/kjQpafhy7Nsk1fqSTyRdriZSYmTQIDAQABAoIBAQC+kJgaCuX8wYAn
|
||||||
|
SXWQ0fmdZlXnMNRpcF0a0pD0SAzGb1RdYBXMaXiqtyhiwc53PPxsCDdNecjayIMd
|
||||||
|
jJVXPTwLhTruOgMS/bp3gcgWwV34UHV4LJXGOGAE+jbS0hbDBMiudOYmj6RmVshp
|
||||||
|
z9G1zZCSQNMXHaWsEYkX59XpzzoB384nRul2QgEtwzUNR9XlpzgtJBLk3SACkvsN
|
||||||
|
mQ/DW8IWHXLg8vLn1LzVJ2e3B16H4MoE2TCHxqfMgr03IDRRJogkenQuQsFhevYT
|
||||||
|
o/mJyHSWavVgzMHG9I5m+eepF4Wyhj1Y4WyKAuMI+9dHAX/h7Lt8XFCQCh5DbkVG
|
||||||
|
zGr34sWBAoGBAOs7n7YZqNaaguovfIdRRsxxZr1yJAyDsr6w3yGImDZYju4c4WY9
|
||||||
|
5esO2kP3FA4p0c7FhQF5oOb1rBuHEPp36cpL4aGeK87caqTfq63WZAujoTZpr9Lp
|
||||||
|
BRbkL7w/xG7jpQ/clpA8sHzHGQs/nelxoOtC7E118FiRgvD/jdhlMyL9AoGBANfX
|
||||||
|
vyoN1pplfT2xR8QOjSZ+Q35S/+SAtMuBnHx3l0qH2bbBjcvM1MNDWjnRDyaYhiRu
|
||||||
|
i+KA7tqfib09+XpB3g5D6Ov7ls/Ldx0S/VcmVWtia2HK8y8iLGtokoBZKQ5AaFX2
|
||||||
|
iQU8+tC4h69GnJYQKqNwgCUzh8+gHX5Y46oDiTmRAoGAYpOx8lX+czB8/Da6MNrW
|
||||||
|
mIZNT8atZLEsDs2ANEVRxDSIcTCZJId7+m1W+nRoaycLTWNowZ1+2ErLvR10+AGY
|
||||||
|
b7Ys79Wg9idYaY9yGn9lnZsMzAiuLeyIvXcSqgjvAKlVWrhOQFOughvNWvFl85Yy
|
||||||
|
oWSCMlPiTLtt7CCsCKsgKuECgYBgdIp6GZsIfkgclKe0hqgvRoeU4TR3gcjJlM9A
|
||||||
|
lBTo+pKhaBectplx9RxR8AnsPobbqwcaHnIfAuKDzjk5mEvKZjClnFXF4HAHbyAF
|
||||||
|
nRzZEy9XkWFhc80T5rRpZO7C7qdxmu2aiKixM3V3L3/0U58qULEDbubHMw9bEhAT
|
||||||
|
PudI8QKBgHEEiMm/hr9T41hbQi/LYanWnlFw1ue+osKuF8bXQuxnnHNuFT/c+9/A
|
||||||
|
vWhgqG6bOEHu+p/IPrYm4tBMYlwsyh4nXCyGgDJLbLIfzKwKAWCtH9LwnyDVhOow
|
||||||
|
GH9shdR+sW3Ew97xef02KAH4VlNANEmBV4sQNqWWvsYrcFm2rOdL
|
||||||
|
-----END RSA PRIVATE KEY-----
|
||||||
@ -0,0 +1,51 @@
|
|||||||
|
-----BEGIN RSA PRIVATE KEY-----
|
||||||
|
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
|
||||||
|
-----END RSA PRIVATE KEY-----
|
||||||
@ -1 +1 @@
|
|||||||
Subproject commit f79ff7099a342261444797ecb4054806c9dcca22
|
Subproject commit fc9b16cb680d8a7b51f4deb0acb03787b42564f7
|
||||||
Loading…
Reference in New Issue