From d7439e15aedb77948e7079fc1e23c2f44e6636ec Mon Sep 17 00:00:00 2001 From: Andrew Gunnerson Date: Sat, 18 May 2024 20:21:29 -0400 Subject: [PATCH] Add support for adding AVB public key to DSU trusted keys This allows the user to boot GSIs signed by the same key. The option is disabled by default because some Android builds disable DSU support by removing all keys to reduce the attack surface. We don't want to reenable DSU support on these builds unless the user asks for it. Closes: #286 Signed-off-by: Andrew Gunnerson --- README.md | 6 ++ avbroot/src/cli/ota.rs | 21 +++++- avbroot/src/patch/boot.rs | 133 +++++++++++++++++++++++++++++++++++++- e2e/e2e.toml | 28 ++++---- e2e/src/config.rs | 5 +- e2e/src/main.rs | 68 +++++++++++-------- 6 files changed, 215 insertions(+), 46 deletions(-) diff --git a/README.md b/README.md index 5074a49..1969a5f 100644 --- a/README.md +++ b/README.md @@ -363,6 +363,12 @@ The only behavior this changes is where the partition is read from. When using ` This has no impact on what patches are applied. For example, when using Magisk, the root patch is applied to the boot partition, no matter if the partition came from the original `payload.bin` or from `--replace`. +### Booting signed GSIs + +Android's [Dynamic System Updates (DSU)](https://developer.android.com/topic/dsu) feature uses a different root of trust than the regular system. Instead of using the bootloader's `avb_custom_key`, it obtains the trusted keys from the `first_stage_ramdisk/avb/*.avbpubkey` files inside the `init_boot` or `vendor_boot` ramdisk. These files are encoded in the same binary format as `avb_pkmd.bin`. + +avbroot can add the custom AVB public key to this directory by passing in `--dsu` when patching an OTA. This allows booting [Generic System Images (GSI)](https://developer.android.com/topic/generic-system-image) signed by the custom AVB key. + ### Clearing vbmeta flags Some Android builds may ship with a root `vbmeta` image with the flags set such that AVB is effectively disabled. When avbroot encounters these images, the patching process will fail with a message like: diff --git a/avbroot/src/cli/ota.rs b/avbroot/src/cli/ota.rs index ec17502..a5b611c 100644 --- a/avbroot/src/cli/ota.rs +++ b/avbroot/src/cli/ota.rs @@ -31,14 +31,16 @@ use crate::{ cli, crypto::{self, PassphraseSource}, format::{ - avb::Header, - avb::{self, Descriptor}, + avb::{self, Descriptor, Header}, ota::{self, SigningWriter, ZipEntry}, padding, payload::{self, PayloadHeader, PayloadWriter}, }, patch::{ - boot::{self, BootImagePatch, MagiskRootPatcher, OtaCertPatcher, PrepatchedImagePatcher}, + boot::{ + self, BootImagePatch, DsuPubKeyPatcher, MagiskRootPatcher, OtaCertPatcher, + PrepatchedImagePatcher, + }, system, }, protobuf::{ @@ -195,6 +197,7 @@ fn patch_boot_images<'a, 'b: 'a>( required_images: &'b RequiredImages, input_files: &mut HashMap, root_patcher: Option>, + dsu: bool, key_avb: &RsaPrivateKey, cert_ota: &Certificate, cancel_signal: &AtomicBool, @@ -206,6 +209,9 @@ fn patch_boot_images<'a, 'b: 'a>( if let Some(p) = root_patcher { boot_patchers.push(p); } + if dsu { + boot_patchers.push(Box::new(DsuPubKeyPatcher::new(key_avb.to_public_key()))); + } let boot_partitions = required_images.iter_boot().collect::>(); @@ -699,6 +705,7 @@ fn patch_ota_payload( writer: impl Write, external_images: &HashMap, root_patcher: Option>, + dsu: bool, clear_vbmeta_flags: bool, key_avb: &RsaPrivateKey, key_ota: &RsaPrivateKey, @@ -751,6 +758,7 @@ fn patch_ota_payload( &required_images, &mut input_files, root_patcher, + dsu, key_avb, cert_ota, cancel_signal, @@ -894,6 +902,7 @@ fn patch_ota_zip( mut zip_writer: &mut ZipWriter, external_images: &HashMap, mut root_patch: Option>, + dsu: bool, clear_vbmeta_flags: bool, key_avb: &RsaPrivateKey, key_ota: &RsaPrivateKey, @@ -1016,6 +1025,7 @@ fn patch_ota_zip( external_images, // There's only one payload in the OTA. root_patch.take(), + dsu, clear_vbmeta_flags, key_avb, key_ota, @@ -1273,6 +1283,7 @@ pub fn patch_subcommand(cli: &PatchCli, cancel_signal: &AtomicBool) -> Result<() &mut zip_writer, &external_images, root_patcher, + cli.dsu, cli.clear_vbmeta_flags, &key_avb, &key_ota, @@ -1841,6 +1852,10 @@ pub struct PatchCli { )] pub ignore_prepatched_compat: u8, + /// Add AVB public key to trusted keys for DSU. + #[arg(long, help_heading = HEADING_OTHER)] + pub dsu: bool, + /// Forcibly clear vbmeta flags if they disable AVB. #[arg(long, help_heading = HEADING_OTHER)] pub clear_vbmeta_flags: bool, diff --git a/avbroot/src/patch/boot.rs b/avbroot/src/patch/boot.rs index ab7b6d2..b8414bb 100644 --- a/avbroot/src/patch/boot.rs +++ b/avbroot/src/patch/boot.rs @@ -23,7 +23,7 @@ use liblzma::{ use rayon::iter::{IntoParallelRefIterator, IntoParallelRefMutIterator, ParallelIterator}; use regex::bytes::Regex; use ring::digest::Context; -use rsa::RsaPrivateKey; +use rsa::{RsaPrivateKey, RsaPublicKey}; use thiserror::Error; use tracing::{debug, debug_span, trace, warn, Span}; use x509_cert::Certificate; @@ -667,6 +667,137 @@ impl BootImagePatch for OtaCertPatcher { } } +/// Add the AVB public key to DSU's list of trusted keys for verifying GSIs. +pub struct DsuPubKeyPatcher { + key: RsaPublicKey, +} + +impl DsuPubKeyPatcher { + const FIRST_STAGE_PATH: &'static [u8] = b"first_stage_ramdisk"; + const DSU_KEYS_PATH: &'static [u8] = b"first_stage_ramdisk/avb"; + const AVBROOT_KEY_PATH: &'static [u8] = b"first_stage_ramdisk/avb/avbroot.avbpubkey"; + + pub fn new(key: RsaPublicKey) -> Self { + Self { key } + } + + fn patch_ramdisk(&self, ramdisk: &mut Vec, cancel_signal: &AtomicBool) -> Result { + let (mut entries, ramdisk_format) = load_ramdisk(ramdisk, cancel_signal)?; + if !entries.iter_mut().any(|e| e.path == Self::FIRST_STAGE_PATH) { + return Ok(false); + } + + if !entries.iter().any(|e| e.path == Self::DSU_KEYS_PATH) { + entries.push(CpioEntry::new_directory(Self::DSU_KEYS_PATH, 0o755)); + } + + let data = CpioEntryData::Data(avb::encode_public_key(&self.key)?); + + if let Some(e) = entries + .iter_mut() + .find(|e| e.path == Self::AVBROOT_KEY_PATH) + { + e.data = data; + } else { + entries.push(CpioEntry::new_file(Self::AVBROOT_KEY_PATH, 0o644, data)); + }; + + *ramdisk = save_ramdisk(&entries, ramdisk_format, cancel_signal)?; + + Ok(true) + } +} + +impl BootImagePatch for DsuPubKeyPatcher { + fn patcher_name(&self) -> &'static str { + "DsuPubKeyPatcher" + } + + fn find_targets<'a>( + &self, + boot_images: &HashMap<&'a str, BootImageInfo>, + cancel_signal: &AtomicBool, + ) -> Result> { + let mut dsu_keys_targets = vec![]; + let mut first_stage_targets = vec![]; + + 'outer: for (name, info) in boot_images { + let ramdisks = match &info.boot_image { + BootImage::V0Through2(b) => slice::from_ref(&b.ramdisk), + BootImage::V3Through4(b) => slice::from_ref(&b.ramdisk), + BootImage::VendorV3Through4(b) => &b.ramdisks, + }; + + for ramdisk in ramdisks { + if ramdisk.is_empty() { + continue; + } + + let (entries, _) = load_ramdisk(ramdisk, cancel_signal)?; + let mut found = false; + + for entry in entries { + if entry.path == Self::DSU_KEYS_PATH { + dsu_keys_targets.push(*name); + found = true; + } else if entry.path == Self::FIRST_STAGE_PATH { + first_stage_targets.push(*name); + found = true; + } + } + + if found { + continue 'outer; + } + } + } + + if !dsu_keys_targets.is_empty() { + // Most builds trust as least one DSU key. For these builds, add the + // user's key to the same directory. + if dsu_keys_targets.len() > 1 { + return Err(Error::Validation(format!( + "DSU keys found in more than one boot image: {dsu_keys_targets:?}", + ))); + } + + Ok(dsu_keys_targets) + } else { + // For builds that don't trust any DSU keys, pick the first boot + // image that contains a first stage ramdisk directory. + if !first_stage_targets.is_empty() { + first_stage_targets.sort(); + first_stage_targets.resize(1, ""); + } + + Ok(first_stage_targets) + } + } + + fn patch(&self, boot_image: &mut BootImage, cancel_signal: &AtomicBool) -> Result<()> { + let ramdisks = match boot_image { + BootImage::V0Through2(b) => slice::from_mut(&mut b.ramdisk), + BootImage::V3Through4(b) => slice::from_mut(&mut b.ramdisk), + BootImage::VendorV3Through4(b) => &mut b.ramdisks, + }; + + for ramdisk in ramdisks { + if ramdisk.is_empty() { + continue; + } + + if self.patch_ramdisk(ramdisk, cancel_signal)? { + return Ok(()); + } + } + + Err(Error::Validation(format!( + "No ramdisk contains {:?}", + Self::FIRST_STAGE_PATH.as_bstr(), + ))) + } +} + /// Replace the boot image with a prepatched boot image if it is compatible. /// /// An image is compatible if all the non-size-related header fields are diff --git a/e2e/e2e.toml b/e2e/e2e.toml index 0026faf..a476069 100644 --- a/e2e/e2e.toml +++ b/e2e/e2e.toml @@ -22,7 +22,7 @@ data.kernel = true avb.signed = true data.type = "boot" data.version = "v4" -data.ramdisks = ["init"] +data.ramdisks = [["init", "first_stage"]] [profile.pixel_v4_gki.partitions.system] avb.signed = false @@ -43,11 +43,11 @@ data.deps = ["system"] avb.signed = false data.type = "boot" data.version = "vendor_v4" -data.ramdisks = ["otacerts"] +data.ramdisks = [["otacerts", "first_stage", "dsu_key_dir"]] [profile.pixel_v4_gki.hashes] -original = "f9477a35e3b60a495e49431c61e3897f11775f453a6a9897ead568357c963618" -patched = "d3436650b4e0c60688dafb1472fa5fe95e67d19a8fad2da4850ffaa739d44574" +original = "24a0a62cc08b96563f4872aee2fdd4a84d1a977b55c326dd9d4ddd92a1d326ea" +patched = "29889670efea78bace221742b19e8ace88b67137fc2f46dcd9dbdf67e4e42267" # Google Pixel 6a # What's unique: boot (boot v4, no ramdisk) + vendor_boot (vendor v4, 2 ramdisks) @@ -77,11 +77,11 @@ data.deps = ["system"] avb.signed = false data.type = "boot" data.version = "vendor_v4" -data.ramdisks = ["init_and_otacerts", "dlkm"] +data.ramdisks = [["init", "otacerts", "first_stage", "dsu_key_dir"], ["dlkm"]] [profile.pixel_v4_non_gki.hashes] -original = "021b4510bc244f5f686fbff89eb2058ec9c96a2949c2fe8caa7750a78d593225" -patched = "0f90ae4c26a54a735d48e13e98bea73b6d1c026b0e95fa5b4b26179a1d6bdc86" +original = "0e7d0924a68d46e00abe96abfa0e5f3a98d5d15a32bef7401b91fca9a19748e8" +patched = "1a2f53d9ac3a1da75e5e7502110bda437c5a725764f16656c564d42460136279" # Google Pixel 4a 5G # What's unique: boot (boot v3) + vendor_boot (vendor v3) @@ -91,7 +91,7 @@ avb.signed = true data.type = "boot" data.version = "v3" data.kernel = true -data.ramdisks = ["init"] +data.ramdisks = [["init"]] [profile.pixel_v3.partitions.system] avb.signed = false @@ -112,11 +112,11 @@ data.deps = ["system"] avb.signed = false data.type = "boot" data.version = "vendor_v3" -data.ramdisks = ["otacerts"] +data.ramdisks = [["otacerts", "first_stage", "dsu_key_dir"]] [profile.pixel_v3.hashes] -original = "12221a69ff32e137d5f19b61f576fc6b33f0973c4a81da7722c640554ff4bc4e" -patched = "0a92969bbd7cb30071a0799eb20028546d1cec3e6ec1ca4d7e1fe7776f1399fc" +original = "533e6f233cb98c98c945044c2ee81a6069e66baee6f7dbcfbf7523795a11215e" +patched = "1eeae9dba0302c2d469bd03c8bccdc0469c171204dbd676a20cb6620d2d11c5c" # Google Pixel 4a # What's unique: boot (boot v2) @@ -126,7 +126,7 @@ avb.signed = false data.type = "boot" data.version = "v2" data.kernel = true -data.ramdisks = ["init_and_otacerts"] +data.ramdisks = [["init", "otacerts", "first_stage", "dsu_key_dir"]] [profile.pixel_v2.partitions.system] avb.signed = false @@ -144,5 +144,5 @@ data.type = "vbmeta" data.deps = ["system"] [profile.pixel_v2.hashes] -original = "8b38d2d999b5b6e240e894f669e9e2643b3764c108d53bb7b02447da725e7c18" -patched = "85b411947145e89cdc7f71b7109a12586f4dbddce3021f0f96172fc465c189d6" +original = "958dfa428abd2901178b90147903d7857ed78d6c016f6cb3af30d024a22a8f9a" +patched = "b0d18ef350ca7b6499b7de4b0182f4ac3be7288ad238ce888708643e750f7631" diff --git a/e2e/src/config.rs b/e2e/src/config.rs index 482336e..779b206 100644 --- a/e2e/src/config.rs +++ b/e2e/src/config.rs @@ -41,7 +41,8 @@ pub struct Avb { pub enum RamdiskContent { Init, Otacerts, - InitAndOtacerts, + FirstStage, + DsuKeyDir, Dlkm, } @@ -62,7 +63,7 @@ pub struct BootData { #[serde(default)] pub kernel: bool, #[serde(default)] - pub ramdisks: Vec, + pub ramdisks: Vec>, } #[derive(Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] diff --git a/e2e/src/main.rs b/e2e/src/main.rs index e256f1e..6b0abeb 100644 --- a/e2e/src/main.rs +++ b/e2e/src/main.rs @@ -227,6 +227,14 @@ fn ramdisk_add_otacerts(entries: &mut Vec, cert_ota: &Certificate) -> Ok(()) } +fn ramdisk_add_first_stage(entries: &mut Vec) { + entries.push(CpioEntry::new_directory(b"first_stage_ramdisk", 0o755)); +} + +fn ramdisk_add_dsu_key_dir(entries: &mut Vec) { + entries.push(CpioEntry::new_directory(b"first_stage_ramdisk/avb", 0o755)); +} + fn ramdisk_add_dlkm(entries: &mut Vec) { for path in [b"lib".as_slice(), b"lib/modules".as_slice()] { entries.push(CpioEntry::new_directory(path, 0o755)); @@ -245,25 +253,29 @@ fn ramdisk_add_dlkm(entries: &mut Vec) { } fn create_ramdisk( - content: RamdiskContent, + content_list: &[RamdiskContent], cert_ota: &Certificate, cancel_signal: &AtomicBool, ) -> Result> { let mut entries = vec![]; - match content { - RamdiskContent::Init => { - ramdisk_add_init(&mut entries); - } - RamdiskContent::Otacerts => { - ramdisk_add_otacerts(&mut entries, cert_ota)?; - } - RamdiskContent::InitAndOtacerts => { - ramdisk_add_init(&mut entries); - ramdisk_add_otacerts(&mut entries, cert_ota)?; - } - RamdiskContent::Dlkm => { - ramdisk_add_dlkm(&mut entries); + for content in content_list { + match content { + RamdiskContent::Init => { + ramdisk_add_init(&mut entries); + } + RamdiskContent::Otacerts => { + ramdisk_add_otacerts(&mut entries, cert_ota)?; + } + RamdiskContent::FirstStage => { + ramdisk_add_first_stage(&mut entries); + } + RamdiskContent::DsuKeyDir => { + ramdisk_add_dsu_key_dir(&mut entries); + } + RamdiskContent::Dlkm => { + ramdisk_add_dlkm(&mut entries); + } } } @@ -298,7 +310,7 @@ fn create_boot_image( let ramdisks = boot_data .ramdisks .iter() - .map(|c| create_ramdisk(*c, cert_ota, cancel_signal)) + .map(|c| create_ramdisk(c, cert_ota, cancel_signal)) .collect::>>()?; let boot_image = match boot_data.version { @@ -361,17 +373,20 @@ fn create_boot_image( ramdisk_metas: boot_data .ramdisks .iter() - .map(|c| match c { - RamdiskContent::Dlkm => RamdiskMeta { - ramdisk_type: bootimage::VENDOR_RAMDISK_TYPE_DLKM, - ramdisk_name: "dlkm".to_owned(), - board_id: Default::default(), - }, - _ => RamdiskMeta { - ramdisk_type: bootimage::VENDOR_RAMDISK_TYPE_PLATFORM, - ramdisk_name: String::new(), - board_id: Default::default(), - }, + .map(|c_list| { + if c_list.iter().any(|c| *c == RamdiskContent::Dlkm) { + RamdiskMeta { + ramdisk_type: bootimage::VENDOR_RAMDISK_TYPE_DLKM, + ramdisk_name: "dlkm".to_owned(), + board_id: Default::default(), + } + } else { + RamdiskMeta { + ramdisk_type: bootimage::VENDOR_RAMDISK_TYPE_PLATFORM, + ramdisk_name: String::new(), + board_id: Default::default(), + } + } }) .collect(), bootconfig: String::new(), @@ -968,6 +983,7 @@ fn patch_image( keys.ota_pass_file.path().as_os_str(), OsStr::new("--cert-ota"), keys.ota_cert_file.path().as_os_str(), + OsStr::new("--dsu"), ]; args.extend_from_slice(extra_args);