动态系统更新

动态系统更新 (DSU) 允许您制作一个 Android 系统映像,用户可以从互联网下载并试用,而不会有损坏当前系统映像的风险。本文档介绍了如何支持 DSU。

内核要求

如需了解内核要求,请参阅实现动态分区

此外,DSU 依赖于 device-mapper-verity (dm-verity) 内核功能来验证 Android 系统映像。因此,您必须启用以下内核配置

  • CONFIG_DM_VERITY=y
  • CONFIG_DM_VERITY_FEC=y

分区要求

从 Android 11 开始,DSU 要求 /data 分区使用 F2FS 或 ext4 文件系统。F2FS 性能更佳,建议使用,但差异应该不明显。

以下是一些关于在 Pixel 设备上进行动态系统更新所需时间的示例

  • 使用 F2FS
    • 109 秒,8G 用户,867M 系统,文件系统类型:F2FS:encryption=aes-256-xts:aes-256-cts
    • 104 秒,8G 用户,867M 系统,文件系统类型:F2FS:encryption=ice
  • 使用 ext4
    • 135 秒,8G 用户,867M 系统,文件系统类型:ext4:encryption=aes-256-xts:aes-256-cts

如果您的平台上耗时更长,您可能需要检查挂载标志是否包含任何导致“同步”写入的标志,或者您可以显式指定“异步”标志以获得更好的性能。

metadata 分区(16 MB 或更大)是存储与已安装映像相关的数据所必需的。它必须在第一阶段挂载期间挂载。

userdata 分区必须使用 F2FS 或 ext4 文件系统。使用 F2FS 时,请包含 Android 通用内核中提供的所有 F2FS 相关补丁。

DSU 是使用 kernel/common 4.9 开发和测试的。建议此功能使用 kernel 4.9 及更高版本。

供应商 HAL 行为

Weaver HAL

Weaver HAL 为存储用户密钥提供了固定数量的插槽。DSU 占用两个额外的密钥插槽。如果 OEM 有 Weaver HAL,则需要有足够的插槽用于通用系统映像 (GSI) 和主机映像。

Gatekeeper HAL

Gatekeeper HAL 需要支持较大的 USER_ID 值,因为 GSI 将 UID 偏移到 HAL,偏移量为 +1000000。

验证启动

如果您想在锁定状态下支持启动开发者 GSI 映像,而无需停用验证启动,请通过将以下行添加到文件 device/<device_name>/device.mk 来添加开发者 GSI 密钥

$(call inherit-product, $(SRC_TARGET_DIR)/product/developer_gsi_keys.mk)

回滚保护

使用 DSU 时,下载的 Android 系统映像必须比设备上当前的系统映像更新。这通过比较Android 验证启动 (AVB) AVB 属性描述符中两个系统映像的安全补丁程序级别来完成:Prop: com.android.build.system.security_patch -> '2019-04-05'

对于不使用 AVB 的设备,请使用引导加载程序将当前系统映像的安全补丁程序级别放入内核 cmdline 或 bootconfig 中:androidboot.system.security_patch=2019-04-05

硬件要求

当您启动 DSU 实例时,将分配两个临时文件

  • 用于存储 GSI.img 的逻辑分区(1~1.5 GB)
  • 一个 8GB 的空 /data 分区,作为运行 GSI 的沙盒

我们建议在启动 DSU 实例之前预留至少 10GB 的可用空间。DSU 还支持从 SD 卡分配。当 SD 卡存在时,它具有最高的分配优先级。SD 卡支持对于可能没有足够内部存储空间的低功耗设备至关重要。当 SD 卡存在时,请确保它未被采纳。DSU 不支持可采纳的 SD 卡

可用的前端

您可以使用 adb、OEM 应用或一键式 DSU 加载器(在 Android 11 或更高版本中)启动 DSU。

使用 adb 启动 DSU

要使用 adb 启动 DSU,请输入以下命令

$ simg2img out/target/product/.../system.img system.raw
$ gzip -c system.raw > system.raw.gz
$ adb push system.raw.gz /storage/emulated/0/Download
$ adb shell am start-activity \
-n com.android.dynsystem/com.android.dynsystem.VerificationActivity  \
-a android.os.image.action.START_INSTALL    \
-d file:///storage/emulated/0/Download/system.raw.gz  \
--el KEY_SYSTEM_SIZE $(du -b system.raw|cut -f1)  \
--el KEY_USERDATA_SIZE 8589934592

使用应用启动 DSU

DSU 的主要入口点是 android.os.image.DynamicSystemClient.java API

public class DynamicSystemClient {


...
...

     /**
     * Start installing DynamicSystem from URL with default userdata size.
     *
     * @param systemUrl A network URL or a file URL to system image.
     * @param systemSize size of system image.
     */
    public void start(String systemUrl, long systemSize) {
        start(systemUrl, systemSize, DEFAULT_USERDATA_SIZE);
    }

您必须在设备上捆绑/预安装此应用。由于 DynamicSystemClient 是系统 API,因此您无法使用常规 SDK API 构建该应用,也无法在 Google Play 上发布它。此应用的目的是

  1. 使用供应商定义的方案获取映像列表和相应的网址。
  2. 将列表中的映像与设备进行匹配,并显示兼容的映像供用户选择。
  3. 像这样调用 DynamicSystemClient.start

    DynamicSystemClient aot = new DynamicSystemClient(...)
       aot.start(
            ...URL of the selected image...,
            ...uncompressed size of the selected image...);
    
    

该网址指向一个 gzipped 非稀疏系统映像文件,您可以使用以下命令制作该文件

$ simg2img ${OUT}/system.img ${OUT}/system.raw
$ gzip ${OUT}/system.raw
$ ls ${OUT}/system.raw.gz

文件名应遵循以下格式

<android version>.<lunch name>.<user defined title>.raw.gz

示例

  • o.aosp_taimen-userdebug.2018dev.raw.gz
  • p.aosp_taimen-userdebug.2018dev.raw.gz

一键式 DSU 加载器

Android 11 引入了一键式 DSU 加载器,它是开发者设置中的一个前端。

Launching the DSU loader

图 1. 启动 DSU 加载器

当开发者点击DSU 加载器按钮时,它会从网络上获取预配置的 DSU JSON 描述符,并在浮动菜单中显示所有适用的映像。选择一个映像以开始 DSU 安装,进度会显示在通知栏上。

DSU image installation progress

图 2. DSU 映像安装进度

默认情况下,DSU 加载器加载包含 GSI 映像的 JSON 描述符。以下部分演示了如何制作 OEM 签名的 DSU 软件包并从 DSU 加载器加载它们。

功能标志

DSU 功能位于 settings_dynamic_android 功能标志下。在使用 DSU 之前,请确保已启用相应的功能标志。

Enabling the feature flag.

图 3. 启用功能标志

功能标志 UI 可能在运行用户版本的设备上不可用。在这种情况下,请改为使用 adb 命令

$ adb shell setprop persist.sys.fflag.override.settings_dynamic_system 1

GCE 上的供应商主机系统映像(可选)

系统映像的可能存储位置之一是 Google Compute Engine (GCE) 存储桶。发布管理员使用 GCP Storage 控制台来添加/删除/更改已发布的系统映像。

映像必须是公开访问的,如此处所示

Public access in GCE

图 4. GCE 中的公开访问

Google Cloud 文档中提供了将项目设为公开的步骤。

ZIP 文件中的多分区的 DSU

从 Android 11 开始,DSU 可以有多个分区。例如,除了 system.img 之外,它还可以包含 product.img。当设备启动时,第一阶段 init 会检测已安装的 DSU 分区,并在启用了已安装的 DSU 时临时替换设备上的分区。DSU 软件包可能包含设备上没有对应分区的分区。

DSU process with multiple partitions

图 5. 具有多个分区的 DSU 流程

OEM 签名的 DSU

为了确保设备上运行的所有映像都经过设备制造商的授权,DSU 软件包中的所有映像都必须签名。例如,假设有一个 DSU 软件包包含两个分区映像,如下所示

dsu.zip {
    - system.img
    - product.img
}

在将 system.imgproduct.img 放入 ZIP 文件之前,必须先使用 OEM 密钥对其进行签名。常见的做法是使用非对称算法,例如 RSA,其中私钥用于签署软件包,公钥用于验证软件包。第一阶段 ramdisk 必须包含配对公钥,例如 /avb/*.avbpubkey。如果设备已采用 AVB,则现有的签名程序就足够了。以下部分说明了签名过程,并重点介绍了用于验证 DSU 软件包中映像的 AVB 公钥的放置位置。

DSU JSON 描述符

DSU JSON 描述符描述 DSU 软件包。它支持两个原语。首先,include 原语包含其他 JSON 描述符或将 DSU 加载器重定向到新位置。例如

{
    "include": ["https://.../gsi-release/gsi-src.json"]
}

其次,image 原语用于描述已发布的 DSU 软件包。在 image 原语内部,有几个属性

  • namedetails 属性是字符串,它们显示在对话框中供用户选择。

  • cpu_apivndkos_version 属性用于兼容性检查,这将在下一节中介绍。

  • 可选的 pubkey 属性描述与用于签署 DSU 软件包的私钥配对的公钥。当指定此属性时,DSU 服务可以检查设备是否具有用于验证 DSU 软件包的密钥。这避免了安装无法识别的 DSU 软件包,例如将 OEM-A 签名的 DSU 安装到 OEM-B 制造的设备上。

  • 可选的 tos 属性指向一个文本文件,该文件描述了相应 DSU 软件包的服务条款。当开发者选择指定了服务条款属性的 DSU 软件包时,会打开图 6 中显示的对话框,要求开发者先接受服务条款,然后才能安装 DSU 软件包。

    Terms of service dialog box

    图 6. 服务条款对话框

作为参考,这是一个 GSI 的 DSU JSON 描述符

{
   "images":[
      {
         "name":"GSI+GMS x86",
         "os_version":"10",
         "cpu_abi": "x86",
         "details":"exp-QP1A.190711.020.C4-5928301",
         "vndk":[
            27,
            28,
            29
         ],
         "pubkey":"",
         "tos": "https://dl.google.com/developers/android/gsi/gsi-tos.txt",
         "uri":"https://.../gsi/gsi_gms_x86-exp-QP1A.190711.020.C4-5928301.zip"
      },
      {
         "name":"GSI+GMS ARM64",
         "os_version":"10",
         "cpu_abi": "arm64-v8a",
         "details":"exp-QP1A.190711.020.C4-5928301",
         "vndk":[
            27,
            28,
            29
         ],
         "pubkey":"",
         "tos": "https://dl.google.com/developers/android/gsi/gsi-tos.txt",
         "uri":"https://.../gsi/gsi_gms_arm64-exp-QP1A.190711.020.C4-5928301.zip"
      },
      {
         "name":"GSI ARM64",
         "os_version":"10",
         "cpu_abi": "arm64-v8a",
         "details":"exp-QP1A.190711.020.C4-5928301",
         "vndk":[
            27,
            28,
            29
         ],
         "pubkey":"",
         "uri":"https://.../gsi/aosp_arm64-exp-QP1A.190711.020.C4-5928301.zip"
      },
      {
         "name":"GSI x86_64",
         "os_version":"10",
         "cpu_abi": "x86_64",
         "details":"exp-QP1A.190711.020.C4-5928301",
         "vndk":[
            27,
            28,
            29
         ],
         "pubkey":"",
         "uri":"https://.../gsi/aosp_x86_64-exp-QP1A.190711.020.C4-5928301.zip"
      }
   ]
}

兼容性管理

以下几个属性用于指定 DSU 软件包与本地设备之间的兼容性

  • cpu_api 是一个字符串,用于描述设备架构。此属性是必需的,并与 ro.product.cpu.abi 系统属性进行比较。它们的值必须完全匹配。

  • os_version 是一个可选整数,用于指定 Android 版本。例如,对于 Android 10,os_version10,对于 Android 11,os_version11。当指定此属性时,它必须等于或大于 ro.system.build.version.release 系统属性。此检查用于防止在 Android 11 供应商设备上启动 Android 10 GSI 映像,目前不支持这样做。允许在 Android 10 设备上启动 Android 11 GSI 映像。

  • vndk 是一个可选数组,用于指定 DSU 软件包中包含的所有 VNDK。当指定此属性时,DSU 加载器会检查从 ro.vndk.version 系统属性中提取的数字是否包含在内。

撤消 DSU 密钥以确保安全

在极少数情况下,当用于签署 DSU 映像的 RSA 密钥对泄露时,应尽快更新 ramdisk 以删除泄露的密钥。除了更新启动分区之外,您还可以使用来自 HTTPS 网址的 DSU 密钥撤销列表(密钥黑名单)来阻止泄露的密钥。

DSU 密钥撤销列表包含已撤销的 AVB 公钥列表。在 DSU 安装期间,DSU 映像中的公钥会使用撤销列表进行验证。如果发现映像包含已撤销的公钥,则 DSU 安装过程将停止。

密钥撤销列表网址应为 HTTPS 网址,以确保安全强度,并在资源字符串中指定

frameworks/base/packages/DynamicSystemInstallationService/res/values/strings.xml@key_revocation_list_url

该字符串的值为 https://dl.google.com/developers/android/gsi/gsi-keyblacklist.json,它是 Google 发布的 GSI 密钥的撤销列表。可以覆盖和自定义此资源字符串,以便采用 DSU 功能的 OEM 可以提供和维护自己的密钥黑名单。这为 OEM 提供了一种在不更新设备 ramdisk 映像的情况下阻止某些公钥的方法。

撤销列表的格式为

{
   "entries":[
      {
         "public_key":"bf14e439d1acf231095c4109f94f00fc473148e6",
         "status":"REVOKED",
         "reason":"Key revocation test key"
      },
      {
         "public_key":"d199b2f29f3dc224cca778a7544ea89470cbef46",
         "status":"REVOKED",
         "reason":"Key revocation test key"
      }
   ]
}
  • public_key 是已撤销密钥的 SHA-1 摘要,格式在在 JSON 描述符中生成 AVB pubkey 属性部分中描述。
  • status 指示密钥的撤销状态。目前,唯一受支持的值是 REVOKED
  • reason 是一个可选字符串,用于描述撤销原因。

DSU 步骤

本部分介绍如何执行多个 DSU 配置步骤。

生成新的密钥对

使用 openssl 命令生成 .pem 格式的 RSA 私钥/公钥对(例如,大小为 2048 位)

$ openssl genrsa -out oem_cert_pri.pem 2048
$ openssl rsa -in oem_cert_pri.pem -pubout -out oem_cert_pub.pem

私钥可能不可访问,并且仅保存在硬件安全模块 (HSM)中。在这种情况下,密钥生成后可能会提供 x509 公钥证书。请参阅将配对 pubkey 添加到 ramdisk部分,以获取有关从 x509 证书生成 AVB 公钥的说明。

要将 x509 证书转换为 PEM 格式

$ openssl x509 -pubkey -noout -in oem_cert_pub.x509.pem > oem_cert_pub.pem

如果证书已经是 PEM 文件,请跳过此步骤。

将配对 pubkey 添加到 ramdisk

必须将 oem_cert.avbpubkey 放在 /avb/*.avbpubkey 下,以验证签名的 DSU 软件包。首先,将 PEM 格式的公钥转换为 AVB 公钥格式

$ avbtool extract_public_key --key oem_cert_pub.pem --output oem_cert.avbpubkey

然后,按照以下步骤将公钥包含在第一阶段 ramdisk 中。

  1. 添加一个预构建模块以复制 avbpubkey。例如,添加 device/<company>/<board>/oem_cert.avbpubkeydevice/<company>/<board>/avb/Android.mk,内容如下

    include $(CLEAR_VARS)
    
    LOCAL_MODULE := oem_cert.avbpubkey
    LOCAL_MODULE_CLASS := ETC
    LOCAL_SRC_FILES := $(LOCAL_MODULE)
    ifeq ($(BOARD_USES_RECOVERY_AS_BOOT),true)
    LOCAL_MODULE_PATH := $(TARGET_RECOVERY_ROOT_OUT)/first_stage_ramdisk/avb
    else
    LOCAL_MODULE_PATH := $(TARGET_RAMDISK_OUT)/avb
    endif
    
    include $(BUILD_PREBUILT)
    
  2. 使 droidcore 目标依赖于添加的 oem_cert.avbpubkey

    droidcore: oem_cert.avbpubkey
    

在 JSON 描述符中生成 AVB pubkey 属性

oem_cert.avbpubkey 采用 AVB 公钥二进制格式。使用 SHA-1 使其可读,然后再将其放入 JSON 描述符中

$ sha1sum oem_cert.avbpubkey | cut -f1 -d ' '
3e62f2be9d9d813ef5........866ac72a51fd20

这将是 JSON 描述符的 pubkey 属性的内容。

   "images":[
      {
         ...
         "pubkey":"3e62f2be9d9d813ef5........866ac72a51fd20",
         ...
      },

签署 DSU 软件包

使用以下方法之一来签署 DSU 软件包

  • 方法 1:重用原始 AVB 签名过程生成的工件来制作 DSU 软件包。另一种方法是从发布软件包中提取已签名的映像,并使用提取的映像直接制作 ZIP 文件。

  • 方法 2:如果私钥可用,请使用以下命令签署 DSU 分区。DSU 软件包(ZIP 文件)中的每个 img 都是单独签名的

    $ key_len=$(openssl rsa -in oem_cert_pri.pem -text | grep Private-Key | sed -e 's/.*(\(.*\) bit.*/\1/')
    $ for partition in system product; do
        avbtool add_hashtree_footer \
            --image ${OUT}/${partition}.img \
            --partition_name ${partition} \
            --algorithm SHA256_RSA${key_len} \
            --key oem_cert_pri.pem
    done
    

有关使用 avbtool 添加 add_hashtree_footer 的更多信息,请参阅使用 avbtool

在本地验证 DSU 软件包

建议使用以下命令针对配对公钥验证所有本地映像


for partition in system product; do
    avbtool verify_image --image ${OUT}/${partition}.img  --key oem_cert_pub.pem
done

预期输出如下所示

Verifying image dsu/system.img using key at oem_cert_pub.pem
vbmeta: Successfully verified footer and SHA256_RSA2048 vbmeta struct in dsu/system.img
: Successfully verified sha1 hashtree of dsu/system.img for image of 898494464 bytes

Verifying image dsu/product.img using key at oem_cert_pub.pem
vbmeta: Successfully verified footer and SHA256_RSA2048 vbmeta struct in dsu/product.img
: Successfully verified sha1 hashtree of dsu/product.img for image of 905830400 bytes

制作 DSU 软件包

以下示例制作了一个包含 system.imgproduct.img 的 DSU 软件包

dsu.zip {
    - system.img
    - product.img
}

在两个映像都签名后,使用以下命令制作 ZIP 文件

$ mkdir -p dsu
$ cp ${OUT}/system.img dsu
$ cp ${OUT}/product.img dsu
$ cd dsu && zip ../dsu.zip *.img && cd -

自定义一键式 DSU

默认情况下,DSU 加载器指向 GSI 映像的元数据,即 https://...google.com/.../gsi-src.json

OEM 可以通过定义指向其自己的 JSON 描述符的 persist.sys.fflag.override.settings_dynamic_system.list 属性来覆盖列表。例如,OEM 可以提供包含 GSI 以及 OEM 专有映像的 JSON 元数据,如下所示

{
    "include": ["https://dl.google.com/.../gsi-src.JSON"]
    "images":[
      {
         "name":"OEM image",
         "os_version":"10",
         "cpu_abi": "arm64-v8a",
         "details":"...",
         "vndk":[
            27,
            28,
            29
         ],
         "spl":"...",
         "pubkey":"",
         "uri":"https://.../....zip"
      },

}

OEM 可以链接已发布的 DSU 元数据,如图 7 所示。

Chaining published DSU metadata

图 7. 链接已发布的 DSU 元数据