动态系统更新

借助动态系统更新 (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 可提供更出色的性能,因此建议使用 F2FS,但两者的区别应该无关紧要。

以下示例说明了 Pixel 设备执行动态系统更新需要多长时间。

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

如果您的平台执行动态系统更新需要的时间长得多,不妨检查装载标志是否包含任何让“sync”写入的标志,或者,您可以明确指定“async”标志以获得更好的性能。

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

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

DSU 是用通用内核 4.9 进行开发和测试的。建议使用内核 4.9 及更高版本来实现此功能。

供应商 HAL 行为

Weaver HAL

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

Gatekeeper HAL

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

启动时验证

如果您希望在不停用启动时验证的情况下支持启动处于 LOCKED 状态开发者 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.5G)
  • 一个 8GB 的空 /data 分区,作为运行 GSI 的沙盒

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

可用的前端

您可以使用 OEM 应用 adb 或一键式 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...);
    
    

该网址指向经过 gzip 压缩的非稀疏系统映像文件,您可以使用以下命令编译此文件:

$ 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 加载器,它是开发者设置中的一个前端。

启动 DSU 加载器

图 1. 启动 DSU 加载器

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

DSU 映像安装进度

图 2. DSU 映像安装进度

默认情况下,DSU 加载器会加载包含 GSI 映像的 JSON 描述符。下面几部分展示了如何创建由 OEM 签名的 DSU 软件包并从 DSU 加载器加载这些软件包。

功能标志

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

启用功能标志。

图 3. 启用功能标志

功能标志界面在运行用户 build 的设备上可能不可用。在这种情况下,请改用 adb 命令:

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

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

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

映像必须可公开访问,如下所示:

在 GCE 中公开访问

图 4. 在 GCE 中公开访问

Google Cloud 文档提供了公开某项内容的过程。

ZIP 文件中的多分区 DSU

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

具有多个分区的 DSU 过程

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

由 OEM 签名的 DSU

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

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

system.imgproduct.img 必须先由 OEM 密钥签名,然后才能添加到 ZIP 文件中。常见做法是使用非对称算法(例如 RSA),其中密钥用于签对软件包进行签名,而公钥用于对其进行验证。第一阶段 ramdisk 必须包含配对公钥,例如 /avb/*.avbpubkey。如果设备已采用 AVB,现有的签名流程足以满足要求。以下各个部分说明了签名流程,重点介绍了如何放置用于对 DSU 软件包中的映像进行验证的 AVB 公钥。

DSU JSON 描述符

DSU JSON 描述符用于描述 DSU 软件包。它支持两个基元。第一个是 include 基元,它包括其他 JSON 描述符,或者会将 DSU 加载器重定向到新位置。例如:

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

第二个是 image 基元,用于描述已发布的 DSU 软件包。映像基元内包含以下几个属性:

  • namedetails 属性 - 它们是对话框上显示的字符串,可供用户选择。

  • cpu_apivndkos_version 属性 - 用于兼容性检查,下一部分将对此进行介绍。

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

  • 可选的 tos 属性 - 指向描述相应 DSU 软件包的服务条款的文本文件。当开发者选择一个指定了服务条款属性的 DSU 软件包时,如图 6 所示的对话框将打开,要求开发者在安装 DSU 软件包之前接受相应服务条款。

    “服务条款”对话框

    图 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,以移除已遭泄露的密钥。除了更新 boot 分区之外,您还可以使用 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 摘要,格式如生成 AVB 公钥部分所述。
  • 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 公钥证书。如需了解如何通过 x509 证书生成 AVB 公钥,请参阅将配对公钥添加到 ramdisk 部分。

如需将 x509 证书转换为 PEM 格式,请运行以下命令:

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

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

将配对公钥添加到 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 公钥属性

oem_cert.avbpubkey 采用 AVB 公钥二进制格式。在将它放入 JSON 描述符之前,使用 SHA-1 使其可读。

$ 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 所示。

链接已发布的 DSU 元数据

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