Skip to content

Commit

Permalink
Add support for incremental OTAs
Browse files Browse the repository at this point in the history
Generating the incremental OTAs is out of scope for Custota, but if
they're already generated some other way, Custota can make use of them.

The vbmeta digest of the current running system is used to select which
incremental OTA to install. This is cryptographically tied to a specific
OS build, so there is no chance that an incremental OTA gets applied to
an incompatible OS. (Unlike eg. if the Android build fingerprint were
used instead.)

Signed-off-by: Andrew Gunnerson <[email protected]>
  • Loading branch information
chenxiaolong committed Dec 14, 2024
1 parent 974f4e3 commit 778b682
Show file tree
Hide file tree
Showing 3 changed files with 126 additions and 26 deletions.
64 changes: 60 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,6 @@ Custota is installed via a Magisk/KernelSU module so that it can run as a system

* The device must support A/B updates.
* This notably excludes all Samsung devices.
* Incremental updates are not supported.
* It would take minimal work to add support, but there's currently no tooling to generate an incremental OTA from two full OTAs.
* Pre-downloading an update to install later is not supported.
* Custota runs `update_engine` in streaming mode, which downloads and installs OTAs at the same time.
* The stock OS' Settings app on Pixel devices always launches the builtin OTA updater.
Expand Down Expand Up @@ -109,6 +107,49 @@ To generate the csig and update info files:

If needed, this file can be easily edited by hand.

### Incremental OTAs

Generating incremental OTAs is out of scope for this project, but if they are generated some other way, Custota can use them. The process is a bit more tedious and requires the following two components:

* The csig file for the full OTA for the source OS version
* The incremental OTA that upgrades the source OS version to the target OS version

With those components available, follow these steps to generate a csig file for the incremental OTA and update the update info JSON file accordingly:

1. Generate a csig file for the incremental OTA. This is the same command as for generating csig files for full OTAs.

```bash
custota-tool \
gen-csig \
--input <incremental OTA>.zip \
--key path/to/ota.key \
--cert path/to/ota.crt
```

2. Get the vbmeta digest of the source full OTA from its csig file:

```bash
custota-tool show-csig -i <source full OTA>.zip.csig
```

To do this programmatically, use `-r` and parse the JSON output. For example:

```
custota-tool show-csig -i <source full OTA>.zip.csig -r | jq -r .vbmeta_digest
```

3. Take the existing update info JSON file (which should already have the full OTA location) and update it with the incremental OTA information. Replace `<source vbmeta digest>` with the digest from the previous step.

```bash
./custota-tool \
gen-update-info \
--file <device codename>.json \
--location <incremental OTA>.zip \
--inc-vbmeta-digest <source vbmeta digest>
```

When checking for updates, Custota will look for an incremental OTA matching the vbmeta digest of the currently running OS. If an incremental OTA does not exist, it will use the full OTA instead.

### HTTPS

To use a self-signed certificate or a custom CA certificate, it needs to be installed into the system CA trust store. To generate a module that does this, run the following command and then flash the generated module zip.
Expand Down Expand Up @@ -231,6 +272,7 @@ The csig file contains a signed JSON message of the form:
},
// ...
],
// Only present for full OTAs, not incremental OTAs.
"vbmeta_digest": "61a7175e883636fb6a4a18139746f7d3ffbf7f5a53e9a4ced6f560a9820ccdb4"
}
```
Expand Down Expand Up @@ -262,9 +304,23 @@ The update info file is a JSON file of the form:
```jsonc
{
"version": 2,
// Location of the latest full OTA.
"full": {
"location_ota": "ota.zip",
"location_csig": "ota.zip.csig"
"location_ota": "full_ota.zip",
"location_csig": "full_ota.zip.csig"
},
// Location of incremental OTAs. The key is the vbmeta digest of the
// currently installed OS that the incremental update applies to. When
// checking for updates, if none of these match, the full OTA will be used.
"incremental": {
"0123456701234567012345670123456701234567012345670123456701234567": {
"location_ota": "incremental_ota_1.zip",
"location_csig": "incremental_ota_1.zip.csig"
},
"89abcdef89abcdef89abcdef89abcdef89abcdef89abcdef89abcdef89abcdef": {
"location_ota": "incremental_ota_2.zip",
"location_csig": "incremental_ota_2.zip.csig"
}
}
}
```
Expand Down
26 changes: 17 additions & 9 deletions app/src/main/java/com/chiller3/custota/updater/UpdaterThread.kt
Original file line number Diff line number Diff line change
Expand Up @@ -513,9 +513,16 @@ class UpdaterThread(
throw IOException("Failed to download update info", e)
}

val otaUri = resolveUri(updateInfoUri, updateInfo.full.locationOta, false)
val vbmetaDigest = SystemPropertiesProxy.get(PROP_VBMETA_DIGEST)
Log.d(TAG, "Current vbmeta digest: $vbmetaDigest")

val locationInfo = updateInfo.incremental[vbmetaDigest] ?: updateInfo.full
val isIncremental = locationInfo !== updateInfo.full
Log.d(TAG, "OTA is incremental: $isIncremental")

val otaUri = resolveUri(updateInfoUri, locationInfo.locationOta, false)
Log.d(TAG, "OTA URI: $otaUri")
val csigUri = resolveUri(updateInfoUri, updateInfo.full.locationCsig, false)
val csigUri = resolveUri(updateInfoUri, locationInfo.locationCsig, false)
Log.d(TAG, "csig URI: $csigUri")

val csigInfo = downloadAndCheckCsig(csigUri)
Expand All @@ -527,15 +534,12 @@ class UpdaterThread(
throw ValidationException("Metadata postcondition lists multiple fingerprints")
}
val fingerprint = metadata.postcondition.getBuild(0)
var updateAvailable = fingerprint != Build.FINGERPRINT
var updateAvailable = isIncremental || fingerprint != Build.FINGERPRINT

// We allow "upgrading" to the same version if the vbmeta digest differs. This happens, for
// example, if a newer version of Magisk was used when patching an OTA with the same OS
// version as what is currently running.
if (!updateAvailable && csigInfo.vbmetaDigest != null) {
val vbmetaDigest = SystemPropertiesProxy.get(PROP_VBMETA_DIGEST)

Log.d(TAG, "Current vbmeta digest: $vbmetaDigest")
Log.d(TAG, "OTA vbmeta digest: ${csigInfo.vbmetaDigest}")
updateAvailable = csigInfo.vbmetaDigest != vbmetaDigest
}
Expand Down Expand Up @@ -714,6 +718,12 @@ class UpdaterThread(
return
}

// We immediately switch to the update state here instead of waiting until
// update_engine begins installation. For incremental OTAs, the payload metadata
// check for verifying source partition digests can take a while and the status
// should not be "checking for updates".
listener.onUpdateProgress(this, ProgressType.UPDATE, 0, 0)

startInstallation(
checkUpdateResult.otaUri,
checkUpdateResult.csigInfo,
Expand Down Expand Up @@ -788,9 +798,7 @@ class UpdaterThread(
val vbmetaDigest: String? = null,
) {
init {
if (version == 2) {
require(vbmetaDigest != null) { "vbmeta_digest must be present in csig version 2" }
} else {
if (version == 1) {
require(vbmetaDigest == null) { "vbmeta_digest is not supported in csig version 1" }
}
}
Expand Down
62 changes: 49 additions & 13 deletions custota-tool/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ use cms::{
signed_data::{EncapsulatedContentInfo, SignedData, SignerIdentifier},
};
use const_oid::ObjectIdentifier;
use hex::FromHexError;
use ring::digest::Digest;
use rsa::{
pkcs1v15::{Signature, SigningKey, VerifyingKey},
Expand Down Expand Up @@ -74,6 +75,22 @@ impl fmt::Debug for VbmetaDigest {
}
}

impl fmt::Display for VbmetaDigest {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&hex::encode(self.0))
}
}

impl FromStr for VbmetaDigest {
type Err = FromHexError;

fn from_str(s: &str) -> Result<Self, Self::Err> {
let mut result = Self([0u8; 32]);
hex::decode_to_slice(s, &mut result.0)?;
Ok(result)
}
}

#[derive(Clone, Debug, Deserialize, Serialize)]
struct CsigInfo {
version: CsigVersion,
Expand Down Expand Up @@ -216,6 +233,10 @@ struct GenerateUpdateInfo {
/// Path to update info file.
#[arg(short, long, value_parser)]
file: PathBuf,

/// Source vbmeta digest for an incremental OTA.
#[arg(short, long, value_name = "SHA256", value_parser)]
inc_vbmeta_digest: Option<VbmetaDigest>,
}

/// Generate a module for system CA certificates.
Expand Down Expand Up @@ -634,17 +655,23 @@ fn subcommand_gen_csig(args: &GenerateCsig, cancel_signal: &AtomicBool) -> Resul
let vbmeta_digest = match args.csig_version {
CsigVersion::Version1 => None,
CsigVersion::Version2 => {
let digest = compute_vbmeta_digest(
raw_reader,
pf_payload_offset,
pf_payload_size,
&header,
cancel_signal,
)?;

info!("vbmeta digest: {}", hex::encode(digest));

Some(VbmetaDigest(digest))
if header.is_full_ota() {
let digest = compute_vbmeta_digest(
raw_reader,
pf_payload_offset,
pf_payload_size,
&header,
cancel_signal,
)?;

info!("vbmeta digest: {}", hex::encode(digest));

Some(VbmetaDigest(digest))
} else {
info!("Skipping vbmeta digest for incremental OTA");

None
}
}
};

Expand Down Expand Up @@ -711,10 +738,19 @@ fn subcommand_gen_update_info(args: &GenerateUpdateInfo) -> Result<()> {
};

update_info.version = 2;
update_info.full = Some(LocationInfo {

let location_info = LocationInfo {
location_ota: args.location.0.clone(),
location_csig: csig_location.into_owned(),
});
};

if let Some(vbmeta_digest) = &args.inc_vbmeta_digest {
update_info
.incremental
.insert(vbmeta_digest.to_string(), location_info);
} else {
update_info.full = Some(location_info);
}

file.seek(SeekFrom::Start(0))?;
file.set_len(0)?;
Expand Down

0 comments on commit 778b682

Please sign in to comment.