mirror of
https://github.com/chenxiaolong/avbroot.git
synced 2026-07-03 14:05:11 +02:00
Compare commits
20 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 46662796a0 | |||
| 237d4e9b11 | |||
| 5adff50b01 | |||
| 905eb9245c | |||
| e22f9f5676 | |||
| 17162df1e7 | |||
| b4ea4dfc9c | |||
| 5572493acf | |||
| 9d38748221 | |||
| fd0408dffe | |||
| af0fb1d9b4 | |||
| 8c965552e8 | |||
| 2532476583 | |||
| cf77999d95 | |||
| 0ade50f49e | |||
| 0bad6f8c6f | |||
| 4ce4863237 | |||
| f615bf48cc | |||
| b505b14574 | |||
| e2adff6eed |
+10
-13
@@ -26,7 +26,7 @@ jobs:
|
||||
- macos-latest
|
||||
steps:
|
||||
- name: Check out repository
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
# For git describe
|
||||
fetch-depth: 0
|
||||
@@ -43,8 +43,6 @@ jobs:
|
||||
- name: Get Rust target triple
|
||||
id: get_target
|
||||
shell: bash
|
||||
env:
|
||||
RUSTC_BOOTSTRAP: '1'
|
||||
run: |
|
||||
echo -n 'name=' >> "${GITHUB_OUTPUT}"
|
||||
rustc -vV | sed -n 's|host: ||p' >> "${GITHUB_OUTPUT}"
|
||||
@@ -77,14 +75,6 @@ jobs:
|
||||
--target ${{ steps.get_target.outputs.name }} \
|
||||
-- test -a -c e2e/e2e.toml
|
||||
|
||||
- name: Archive documentation
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: avbroot-${{ steps.get_version.outputs.version }}-${{ steps.get_target.outputs.name }}
|
||||
path: |
|
||||
LICENSE
|
||||
README.md
|
||||
|
||||
# Due to https://github.com/rust-lang/rust/issues/78210, we have to use
|
||||
# the --target option, which puts all output files in a different path.
|
||||
# Symlink that path to the normal output directory so that we don't need
|
||||
@@ -95,11 +85,18 @@ jobs:
|
||||
rm -rf target/output
|
||||
ln -s ${{ steps.get_target.outputs.name }}/release target/output
|
||||
|
||||
# This is separate so we can have a flat directory structure.
|
||||
# This is done to ensure a flat directory structure. The upload-artifact
|
||||
# action no longer allows multiple uploads to the same destination.
|
||||
- name: Copy documentation to target directory
|
||||
shell: bash
|
||||
run: cp LICENSE README.md target/output/
|
||||
|
||||
- name: Archive executable
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: avbroot-${{ steps.get_version.outputs.version }}-${{ steps.get_target.outputs.name }}
|
||||
path: |
|
||||
target/output/LICENSE
|
||||
target/output/README.md
|
||||
target/output/avbroot
|
||||
target/output/avbroot.exe
|
||||
|
||||
@@ -11,7 +11,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out repository
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Run cargo-deny
|
||||
uses: EmbarkStudios/cargo-deny-action@v1
|
||||
|
||||
@@ -25,14 +25,12 @@ jobs:
|
||||
echo "version=${version}" >> "${GITHUB_OUTPUT}"
|
||||
|
||||
- name: Check out repository
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Create release
|
||||
uses: softprops/action-gh-release@v1
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
tag_name: v${{ steps.get_version.outputs.version }}
|
||||
name: Version ${{ steps.get_version.outputs.version }}
|
||||
body_path: RELEASE.md
|
||||
draft: true
|
||||
prerelease: false
|
||||
|
||||
@@ -7,6 +7,17 @@
|
||||
to update the actual links at the bottom of the file.
|
||||
-->
|
||||
|
||||
### Version 3.1.2
|
||||
|
||||
* Use `fastboot flashall` for initial setup to avoid needing to manually flash every partition ([PR #253])
|
||||
* Remove binary test files in the git repo and generate them at runtime ([Issue #265], [PR #276])
|
||||
* Fix portions of a couple error messages being incorrectly quoted ([PR #277])
|
||||
|
||||
### Version 3.1.1
|
||||
|
||||
* Cache salted SHA-256 contexts for a small performance improvement ([PR #257])
|
||||
* Fix loading certificates that have extra text outside of the marker lines ([PR #261])
|
||||
|
||||
### Version 3.1.0
|
||||
|
||||
* The `OEMUnlockOnBoot` module has been split out to a separate repo ([Discussion #235], [PR #246])
|
||||
@@ -169,6 +180,7 @@ Behind-the-scenes changes:
|
||||
[Issue #222]: https://github.com/chenxiaolong/avbroot/issues/222
|
||||
[Issue #223]: https://github.com/chenxiaolong/avbroot/issues/223
|
||||
[Issue #225]: https://github.com/chenxiaolong/avbroot/issues/225
|
||||
[Issue #265]: https://github.com/chenxiaolong/avbroot/issues/265
|
||||
[PR #130]: https://github.com/chenxiaolong/avbroot/pull/130
|
||||
[PR #132]: https://github.com/chenxiaolong/avbroot/pull/132
|
||||
[PR #133]: https://github.com/chenxiaolong/avbroot/pull/133
|
||||
@@ -242,5 +254,10 @@ Behind-the-scenes changes:
|
||||
[PR #246]: https://github.com/chenxiaolong/avbroot/pull/246
|
||||
[PR #247]: https://github.com/chenxiaolong/avbroot/pull/247
|
||||
[PR #251]: https://github.com/chenxiaolong/avbroot/pull/251
|
||||
[PR #253]: https://github.com/chenxiaolong/avbroot/pull/253
|
||||
[PR #255]: https://github.com/chenxiaolong/avbroot/pull/255
|
||||
[PR #256]: https://github.com/chenxiaolong/avbroot/pull/256
|
||||
[PR #257]: https://github.com/chenxiaolong/avbroot/pull/257
|
||||
[PR #261]: https://github.com/chenxiaolong/avbroot/pull/261
|
||||
[PR #276]: https://github.com/chenxiaolong/avbroot/pull/276
|
||||
[PR #277]: https://github.com/chenxiaolong/avbroot/pull/277
|
||||
|
||||
Generated
+4
-4
@@ -108,7 +108,7 @@ checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
|
||||
|
||||
[[package]]
|
||||
name = "avbroot"
|
||||
version = "3.1.0"
|
||||
version = "3.1.2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"assert_matches",
|
||||
@@ -532,7 +532,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "e2e"
|
||||
version = "3.1.0"
|
||||
version = "3.1.2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"avbroot",
|
||||
@@ -626,7 +626,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "fuzz"
|
||||
version = "3.1.0"
|
||||
version = "3.1.2"
|
||||
dependencies = [
|
||||
"avbroot",
|
||||
"honggfuzz",
|
||||
@@ -2045,7 +2045,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "xtask"
|
||||
version = "3.1.0"
|
||||
version = "3.1.2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"clap",
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@ members = ["avbroot", "e2e", "fuzz", "xtask"]
|
||||
resolver = "2"
|
||||
|
||||
[workspace.package]
|
||||
version = "3.1.0"
|
||||
version = "3.1.2"
|
||||
license = "GPL-3.0-only"
|
||||
edition = "2021"
|
||||
repository = "https://github.com/chenxiaolong/avbroot"
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# avbroot
|
||||
|
||||
(This page is also available in: [Russian (Русский)](./README.ru.md).)
|
||||
|
||||
avbroot is a program for patching Android A/B-style OTA images for root access while preserving AVB (Android Verified Boot) using custom signing keys. It is compatible with both Magisk and KernelSU. If desired, it can also just re-sign an OTA without enabling root access.
|
||||
|
||||
Having a good understanding of how AVB and A/B OTAs work is recommended prior to using avbroot. At the very least, please make sure the [warnings and caveats](#warnings-and-caveats) are well-understood to avoid the risk of hard bricking.
|
||||
@@ -25,7 +27,7 @@ avbroot applies the following patches to the partition images:
|
||||
|
||||
* The `boot`, `recovery`, or `vendor_boot` image, depending on device, is patched to replace the OTA signature verification certificates with the custom OTA signing certificate. This allows future patched OTAs to be sideloaded from recovery mode after the bootloader has been locked. It also prevents accidental flashing of the original unpatched OTA.
|
||||
|
||||
* The `system` or `my_engineering` image, depending on device, is also patched to replace the OTA signature verification certificates. This prevents the OS' system updater app from installing an unpatched OTA and also allows the use of custom OTA updater apps.
|
||||
* The `system` image is also patched to replace the OTA signature verification certificates. This prevents the OS' system updater app from installing an unpatched OTA and also allows the use of custom OTA updater apps.
|
||||
|
||||
## Warnings and Caveats
|
||||
|
||||
@@ -126,12 +128,16 @@ When patching OTAs for multiple devices, generating unique keys for each device
|
||||
|
||||
The commands above are provided for convenience. avbroot is compatible with any standard PKCS8-encoded 4096-bit RSA private key and PEM-encoded X509 certificate, like those generated by openssl.
|
||||
|
||||
If you lose your AVB or OTA signing key, you will no longer be able to sign new OTA zips. You will have to generate new signing keys and unlock your bootloader again (including a data wipe). Follow the [Usage section](#usage) as if doing an initial setup.
|
||||
If you lose your AVB or OTA signing key, you will no longer be able to sign new OTA zips. You will have to generate new signing keys and unlock your bootloader again (triggering a data wipe). Follow the [Usage section](#usage) as if doing an initial setup.
|
||||
|
||||
## Initial setup
|
||||
|
||||
1. Reboot into fastboot mode and unlock the bootloader if it isn't already unlocked. This will trigger a data wipe.
|
||||
|
||||
```bash
|
||||
fastboot flashing unlock
|
||||
```
|
||||
|
||||
2. When setting things up for the first time, the device must already be running the correct OS. Flash the original unpatched OTA if needed.
|
||||
|
||||
3. Extract the partition images from the patched OTA that are different from the original.
|
||||
@@ -139,27 +145,26 @@ If you lose your AVB or OTA signing key, you will no longer be able to sign new
|
||||
```bash
|
||||
avbroot ota extract \
|
||||
--input /path/to/ota.zip.patched \
|
||||
--directory extracted
|
||||
--directory extracted \
|
||||
--fastboot
|
||||
```
|
||||
|
||||
If you prefer to extract and flash all OS partitions just to be safe, pass in `--all`.
|
||||
|
||||
4. Flash the partition images that were extracted.
|
||||
|
||||
For each partition inside `extracted/`, except for `system`, run:
|
||||
|
||||
```bash
|
||||
fastboot flash <partition> extracted/<partition>.img
|
||||
ANDROID_PRODUCT_OUT=extracted fastboot flashall --skip-reboot
|
||||
```
|
||||
|
||||
Then, reboot into recovery's fastbootd mode and flash `system`:
|
||||
|
||||
```bash
|
||||
fastboot reboot fastboot
|
||||
fastboot flash system extracted/system.img
|
||||
```
|
||||
|
||||
5. Set up the custom AVB public key in the bootloader.
|
||||
Note that this only flashes the OS partitions. The bootloader and modem/radio partitions are left untouched due to fastboot limitations. If they are not already up to date or if unsure, after fastboot completes, follow the steps in the [updates section](#updates) to sideload the patched OTA once. Sideloading OTAs always ensures that all partitions are up to date.
|
||||
|
||||
Alternatively, for Pixel devices, running `flash-base.sh` from the factory image will also update the bootloader and modem.
|
||||
|
||||
5. Set up the custom AVB public key in the bootloader after rebooting from fastbootd to bootloader.
|
||||
|
||||
```bash
|
||||
fastboot reboot-bootloader
|
||||
fastboot erase avb_custom_key
|
||||
fastboot flash avb_custom_key /path/to/avb_pkmd.bin
|
||||
```
|
||||
@@ -180,6 +185,12 @@ If you lose your AVB or OTA signing key, you will no longer be able to sign new
|
||||
|
||||
7. Reboot back into fastboot and lock the bootloader. This will trigger a data wipe again.
|
||||
|
||||
```bash
|
||||
fastboot flashing lock
|
||||
```
|
||||
|
||||
Confirm by pressing volume down and then power. Then reboot.
|
||||
|
||||
Remember: **Do not uncheck `OEM unlocking`!**
|
||||
|
||||
**WARNING**: If you are flashing CalyxOS, the setup wizard will [automatically turn off the `OEM unlocking` switch](https://github.com/CalyxOS/platform_packages_apps_SetupWizard/blob/7d2df25cedcbff83ddb608e628f9d97b38259c26/src/org/lineageos/setupwizard/SetupWizardApp.java#L135-L140). Make sure to manually reenable it again from Android's developer settings. Consider using the [`OEMUnlockOnBoot` module](https://github.com/chenxiaolong/OEMUnlockOnBoot) to automatically ensure OEM unlocking is enabled on every boot.
|
||||
|
||||
+460
@@ -0,0 +1,460 @@
|
||||
# avbroot
|
||||
|
||||
avbroot – это программа для модификации OTA-образов Android A/B-формата с целью получения root-прав при сохранении прохождения AVB (Android Verified Boot) с использованием кастомных (пользовательских) ключей подписи. Она совместима как с Magisk, так и с KernelSU. При необходимости можно просто переподписать OTA, без получения root-доступа.
|
||||
|
||||
Прежде чем использовать avbroot, рекомендуется иметь хорошее понимание того, как работают AVB и OTA в формате A/B. Как минимум, следует ознакомиться с [разделом предостережений,](#предостережения) чтобы избежать хардбрика устройства.
|
||||
|
||||
**ПРИМЕЧАНИЕ:** avbroot 2.0 была переписана на Rust и больше не имеет в основе никакого кода AOSP, а CLI полностью обратно совместим. Тем не менее, старую реализацию на Python можно найти в одноименной ветке `python`.
|
||||
|
||||
## Требования
|
||||
|
||||
* Поддерживаются только устройства, использующие современную A/B-разметку. Это большинство девайсов, выпускаемых с Android 10 и новее (за исключением устройств от Samsung). Чтобы проверить, использует ли ваш телефон необходимую схему разметки, откройте zip-архив OTA и проверьте:
|
||||
|
||||
* наличие файла `payload.bin` (обычно находится в корне архива)
|
||||
* наличие файла `META-INF/com/android/metadata` (Android 10-11) или `META-INF/com/android/metadata.pb` (Android 12+)
|
||||
|
||||
* Устройство должно поддерживать установку пользовательского публичного ключа для подтверждения статуса доверия загрузчика. Обычно это делается с помощью команды `fastboot flash avb_custom_key`. Все устройства Pixel с разблокируемым загрузчиком, начиная с Pixel 2, поддерживают эту функцию. Другие устройства тоже могут поддерживать её, но достоверно убедиться в этом можно только проверив лично.
|
||||
|
||||
* ПРИМЕЧАНИЕ: Некоторые девайсы от OnePlus имеют некорректную реализацию, где возможна установка кастомного публичного ключа, но устройство все равно не будет загружаться, несмотря на наличие корректной подписи. Понижение загрузчика до версии, поставляемой вместе с Android 11, потенциально может помочь. Уже сообщалось о наличии этой проблемы на нескольких устройствах OnePlus ([#186,](https://github.com/chenxiaolong/avbroot/issues/186) [#195,](https://github.com/chenxiaolong/avbroot/discussions/195) [#212](https://github.com/chenxiaolong/avbroot/issues/212)).
|
||||
|
||||
## Патчи
|
||||
|
||||
avbroot модифицирует следующие образы:
|
||||
|
||||
* `boot` или `init_boot`, в зависимости от устройства, модифицируется для получения root-доступа. В случае с Magisk, патч будет эквивалентен тому, что производится в самом приложении Magisk.
|
||||
|
||||
* `boot`, `recovery` или `vendor_boot`, в зависимости от устройства, модифицируется для замены сертификата проверки подписи OTA на пользовательский. Это позволяет устанавливать будущие пропатченные OTA через режим Recovery уже после блокировки загрузчика, то есть в качестве обновления. Также это предотвращает случайную установку оригинального непропатченного OTA.
|
||||
|
||||
* `system` тоже модифицируется для замены сертификата проверки подписи OTA. Это не позволит системному приложению обновлений ОС установить оригинальный непропатченный OTA и дает возможность использовать сторонние приложения для установки пропатченных OTA.
|
||||
|
||||
## Предостережения
|
||||
|
||||
* **Всегда оставляйте опцию `Заводской разблокировки`** (или OEM unlocking в англ.) **включенной при наличии root-прав с заблокированным загрузчиком.** Это очень важно. Доступ к root-правам потенциально позволяет перезаписать загрузочный раздел из-под системы, будь то сделано случайно или намеренно, файлом, который не был подписан должным образом. В таком случае, система и режим Recovery больше не смогут загрузиться, а команда `fastboot flashing unlock` будет недоступна, потому что параметр Заводской разблокировки отключен. То есть, это приведет к **_хардбрику устройства_**.
|
||||
|
||||
Повторюсь: **_ВСЕГДА оставляйте `Заводскую разблокировку` включенной при наличии root-прав._**
|
||||
|
||||
* Любая операция, приводящая к прошивке некорректно подписанного загрузочного образа, приведет к тому, что устройство больше не сможет загрузиться в систему/режим Recovery, а для его восстановления потребуется повторная разблокировка загрузчика (и, следовательно, стирание всех пользовательских данных). Это же относится и к методу `Прямой установки` для обновления Magisk. Обновление Magisk **должно выполняться только путем обновления OTA,** а не через Magisk Manager.
|
||||
|
||||
Если в загрузочный раздел были внесены какие-либо изменения, **не перезагружайтесь**. Обратитесь за помощью, [открыв Issue,](https://github.com/chenxiaolong/avbroot/issues/new) и четко разъясните, какие конкретные действия привели к возникновению такой ситуации. Если Android всё еще работает и доступ к root-правам сохранился – вероятно, получится откатить изменения до исходного состояния, не стирая ваши данные.
|
||||
|
||||
## Использование
|
||||
|
||||
1. Убедитесь, что вы ознакомились и поняли указанные выше [предостережения.](#предостережения)
|
||||
|
||||
2. Скачайте последнюю версию со страницы [релизов.](https://github.com/chenxiaolong/avbroot/releases) Чтобы сверить цифровую подпись, см. раздел [проверки цифровых подписей.](#проверка-цифровых-подписей)
|
||||
|
||||
avbroot – это отдельный исполняемый файл. Он не требует установки и может быть запущен из любого места на диске.
|
||||
|
||||
3. [Сгенерируйте ключи подписи.](#генерация-ключей)
|
||||
|
||||
4. Пропатчите ОТА-архив с помощью команды:
|
||||
|
||||
```bash
|
||||
avbroot ota patch \
|
||||
--input /путь/к/ota.zip \
|
||||
--key-avb /путь/к/avb.key \
|
||||
--key-ota /путь/к/ota.key \
|
||||
--cert-ota /путь/к/ota.crt \
|
||||
```
|
||||
|
||||
Добавьте следующие аргументы в конец команды в зависимости от того, как вы хотите получить root-доступ.
|
||||
|
||||
* Для получения root-доступа с использованием Magisk:
|
||||
|
||||
```bash
|
||||
--magisk /путь/к/magisk.apk \
|
||||
--magisk-preinit-device <имя>
|
||||
```
|
||||
|
||||
Если вы не знаете имени раздела предварительной инициализации Magisk, следуйте инструкции [в соответствующем разделе.](#предварительная-инициализация-устройства-для-magisk)
|
||||
|
||||
Если вы пропатчили загрузочный образ вручную через приложение Magisk (вместо автоматического идентичного патчинга через avbroot), используйте следующий аргумент:
|
||||
|
||||
```bash
|
||||
--prepatched /путь/к/magisk_patched-xxxxx_yyyyy.img
|
||||
```
|
||||
|
||||
* Для получения root-доступа с использованием KernelSU:
|
||||
|
||||
```bash
|
||||
--prepatched /путь/к/kernelsu_boot.img
|
||||
```
|
||||
|
||||
* Без root-доступа:
|
||||
|
||||
```bash
|
||||
--rootless
|
||||
```
|
||||
|
||||
Больше информации про существующие аргументы можно найти в разделе [расширенного использования.](#расширенное-использование)
|
||||
|
||||
Если название для `--output` не указывается, то готовый файл будет записан как `<название-ota-zip-в-input>.patched`.
|
||||
|
||||
5. Готово! Для прошивки пропатченного OTA следуйте инструкции в разделе [первоначальной настройки.](#первоначальная-настройка) Для последующих обновлений тоже есть соответствующий [раздел обновлений.](#обновления)
|
||||
|
||||
## Генерация ключей
|
||||
|
||||
Во время патчинга OTA, avbroot подписывает несколько компонентов:
|
||||
|
||||
* загрузочный образ (boot)
|
||||
* образ vbmeta
|
||||
* payload из OTA
|
||||
* сам архив OTA
|
||||
|
||||
Первые два компонента подписываются ключом AVB, а последние два – ключом OTA. Можно использовать один и тот же ключ, однако в следующих шагах описано, как сгенерировать два отдельных.
|
||||
|
||||
Если вы патчите OTA сразу для нескольких устройств, настоятельно рекомендуется генерировать уникальные ключи для каждого девайса – так вы защитите себя от случайной прошивки неподходящего OTA для другого телефона.
|
||||
|
||||
1. Сгенерируйте ключи подписи для AVB и OTA.
|
||||
|
||||
```bash
|
||||
avbroot key generate-key -o avb.key
|
||||
avbroot key generate-key -o ota.key
|
||||
```
|
||||
|
||||
2. Преобразуйте публичную часть ключа подписи AVB в формат метаданных публичного ключа AVB. Именно этот формат используется в загрузчике устройства для установки пользовательского ключа.
|
||||
|
||||
```bash
|
||||
avbroot key extract-avb -k avb.key -o avb_pkmd.bin
|
||||
```
|
||||
|
||||
3. Сгенерируйте самоподписанный сертификат для ключа подписи OTA. Он используется режимом Recovery для проверки подписи OTA при сайдлоадинге обновления.
|
||||
|
||||
```bash
|
||||
avbroot key generate-cert -k ota.key -o ota.crt
|
||||
```
|
||||
|
||||
avbroot совместим с любым стандартным 4096-битным приватным ключом RSA в кодировке PKCS8 и сертификатом X509 в кодировке PEM, например с теми, которые генерируются openssl.
|
||||
|
||||
Если вы потеряете ключ(-и) подписи AVB или OTA, вы больше не сможете подписывать новые OTA-архивы. Придется генерировать новые ключи подписи и разблокировать загрузчик (что приведет к стиранию всех данных). В таком случае возвращайтесь к инструкции в разделе [использования.](#использование)
|
||||
|
||||
## Первоначальная настройка
|
||||
|
||||
1. Перезагрузитесь в режим fastboot и разблокируйте загрузчик, если не сделали этого ранее. Это приведет к стиранию всех пользовательских данных.
|
||||
|
||||
```bash
|
||||
fastboot flashing unlock
|
||||
```
|
||||
|
||||
2. Перед первой установкой, на устройстве уже должна быть установлена в оригинальном виде та прошивка, пропатченную версию которой вы собираетесь ставить. Если это не так, сначала установите оригинальную непропатченную OTA.
|
||||
|
||||
3. Извлекаем из пропатченного OTA модифицированные образы:
|
||||
|
||||
```bash
|
||||
avbroot ota extract \
|
||||
--input /путь/к/ota.zip.patched \
|
||||
--directory extracted \
|
||||
--fastboot
|
||||
```
|
||||
|
||||
Если вы на всякий случай хотите прошить вообще все разделы из ОТА, извлечь их можно, указав аргумент `--all`.
|
||||
|
||||
4. Прошейте извлеченные образы разделов.
|
||||
|
||||
```bash
|
||||
ANDROID_PRODUCT_OUT=extracted fastboot flashall --skip-reboot
|
||||
```
|
||||
|
||||
Обратите внимание, что так прошиваются лишь те образы, что относятся к системе. Разделы загрузчика и модема же остаются нетронутыми из-за ограничений fastboot. Если они не обновлены до необходимой версии, или вы не уверены в этом, после прошивки перейдите к пункту [обновлений](#обновления) и установите пропатченный OTA-архив сайдлоадом в режиме Recovery. Прошивка полного OTA гарантирует, что абсолютно все разделы будут обновлены.
|
||||
|
||||
Для устройств Pixel есть ещё один вариант: запуск скрипта `flash-base.sh` из папки заводских образов (factory images) обновит загрузчик и модем.
|
||||
|
||||
5. После перезагрузки из fastbootd в загрузчик (bootloader), установите пользовательский публичный ключ AVB в загрузчик:
|
||||
|
||||
```bash
|
||||
fastboot reboot-bootloader
|
||||
fastboot erase avb_custom_key
|
||||
fastboot flash avb_custom_key /путь/к/avb_pkmd.bin
|
||||
```
|
||||
|
||||
6. **[Опционально]** Перед блокировкой загрузчика загрузитесь в систему, дабы убедиться, что все подписано правильно.
|
||||
|
||||
Установите приложение Magisk или KernelSU и выполните следующую команду:
|
||||
|
||||
```bash
|
||||
adb shell su -c 'dmesg | grep libfs_avb'
|
||||
```
|
||||
|
||||
Если AVB работает корректно, будет выведено следующее сообщение:
|
||||
|
||||
```bash
|
||||
init: [libfs_avb]Returning avb_handle with status: Success
|
||||
```
|
||||
|
||||
7. Перезагрузитесь в fastboot и заблокируйте загрузчик. Это снова приведет к стиранию данных.
|
||||
|
||||
```bash
|
||||
fastboot flashing lock
|
||||
```
|
||||
|
||||
Подтвердите нажатием клавиш уменьшения громкости и включения, а после перезагрузитесь в систему.
|
||||
|
||||
Напоминаю: **не отключайте `Заводскую разблокировку`!**
|
||||
|
||||
**ПРЕДУПРЕЖДЕНИЕ**: Если вы прошили CalyxOS, мастер настройки [автоматически отключит опцию `Заводской разблокировки`.](https://github.com/CalyxOS/platform_packages_apps_SetupWizard/blob/7d2df25cedcbff83ddb608e628f9d97b38259c26/src/org/lineageos/setupwizard/SetupWizardApp.java#L135-L140) Не забудьте снова включить её вручную в настройках для разработчиков. Для перестраховки можете использовать [модуль `OEMUnlockOnBoot`,](https://github.com/chenxiaolong/OEMUnlockOnBoot) который автоматически включает пункт Заводской разблокировки при каждом запуске системы.
|
||||
|
||||
8. Готово. Установка последующих обновлений системы, Magisk или KernelSU, описывается в [следующем разделе.](#обновления)
|
||||
|
||||
## Обновления
|
||||
|
||||
Обновления Android, Magisk и KernelSU выполняются одинаково – исключительно путем обновления или репатчинга того же самого OTA.
|
||||
|
||||
1. Если Magisk или KernelSU обновились, сначала установите их новый `.apk`. Если вы случайно открыли приложение после обновления, убедитесь, что оно не начало прошивать загрузочный образ. Если появится предложение обновить сам загрузочный образ – отклоните его.
|
||||
|
||||
2. Следуйте инструкции в разделе [использования,](#использование) чтобы пропатчить OTA уже с новым .apk Magisk'а/предварительно пропатченным образом с Magisk или KernelSU.
|
||||
|
||||
3. Перезагрузитесь в режим Recovery. Если устройство повисло на сплеше с сообщением "No command", удерживайте кнопку питания, а затем нажмите кнопку увеличения громкости один раз.
|
||||
|
||||
4. Обновитесь (Apply update from adb → `adb sideload <ota.zip.patched>`).
|
||||
|
||||
5. Готово!
|
||||
|
||||
## Возврат на заводскую прошивку
|
||||
|
||||
Если вы хотите отказаться от использования avbroot и вернуться на стоковую прошивку:
|
||||
|
||||
1. Перезагрузитесь в режим fastboot и разблокируйте загрузчик. Это приведет к стиранию всех пользовательских данных.
|
||||
|
||||
2. Удалите пользовательский публичный ключ AVB.
|
||||
|
||||
```bash
|
||||
fastboot erase avb_custom_key
|
||||
```
|
||||
|
||||
3. Прошейте стоковую прошивку. Готово.
|
||||
|
||||
## OTA-обновления
|
||||
|
||||
avbroot заменяет `/system/etc/security/otacerts.zip` в разделах системы и Recovery на новый архив, содержащий пользовательский сертификат подписи OTA. Это предотвращает случайную установку непропатченных OTA как при загрузке в Android, так и при сайдлоадинге через Recovery.
|
||||
|
||||
Рекомендуется отключить приложение обновлений системы, чтобы оно не пыталось установить непропатченные OTA:
|
||||
|
||||
* Стоковая прошивка: Отключите `Автоматические обновления системы` (Automatic system updates в англ.) в настройках для разработчиков.
|
||||
* Кастомная прошивка: Отключите приложение обновлений системы (или запретите ему доступ к Интернету) через Настройки -> Приложения -> Все приложения -> (меню/три точки) -> Показать системные -> (найдите приложение обновлений, например Обновления системы/Updater).
|
||||
|
||||
Это особенно важно для некоторых кастомных прошивок, поскольку их фирменное приложение для обновления системы может уйти в бесконечный цикл, загружая OTA-обновление, а затем повторяя попытку загрузки и установки при неудачной проверке подписи.
|
||||
|
||||
Если вы хотите поднять собственный сервер для ОТА-обновлений, вам может быть интересно приложение [Custota.](https://github.com/chenxiaolong/Custota)
|
||||
|
||||
## Режим обслуживания
|
||||
|
||||
Некоторые устройства поставляются с режимом обслуживания, который загружает систему с чистым образом `userdata`, благодаря чему специалист по ремонту может проводить диагностику устройства, не имея доступа к пользовательским данным владельца.
|
||||
|
||||
Если на устройстве есть root-права, использовать этот режим небезопасно. Если у вас обычная сборка Magisk/KernelSU, не подписанная вашим собственным ключом, кто угодно может установить официальное приложение Magisk/KernelSU в режиме обслуживания и запросто получить root-права без какой-либо аутентификации.
|
||||
|
||||
Потому, чтобы безопасно использовать режим обслуживания:
|
||||
|
||||
1. Отключите root-доступ на устройстве, пропатчив OTA с аргументом `--rootless` (вместо `--magisk` или `--prepatched`) и прошив его.
|
||||
|
||||
2. Включите режим обслуживания.
|
||||
|
||||
3. Получив отремонтированное устройство обратно, выйдите из режима обслуживания.
|
||||
|
||||
4. Прошейте рутированный OTA в обычном режиме.
|
||||
|
||||
Поскольку удаление root-прав и повторное их получение выполняются путем перепрошивки OTA, данные устройства стёрты не будут.
|
||||
|
||||
## Предварительная инициализация устройства для Magisk
|
||||
|
||||
Magisk версии 25211 и новее требует наличие раздела, доступного для записи пользовательских правил SELinux, к которым необходимо обращаться на ранних этапах загрузки. Его можно определить только на реальном устройстве, поэтому avbroot требует указания точного названия с помощью аргумента `--magisk-preinit-device <имя>`. Чтобы получить имя раздела:
|
||||
|
||||
1. Извлеките загрузочный образ из оригинального, непропатченного OTA:
|
||||
|
||||
```bash
|
||||
avbroot ota extract \
|
||||
--input /path/to/ota.zip \
|
||||
--directory . \
|
||||
--boot-only
|
||||
```
|
||||
|
||||
2. Теперь нужно пропатчить загрузочный образ с помощью приложения Magisk. Это **ДОЛЖНО** быть сделано именно на целевом устройстве или устройстве той же модели! Имя раздела будет неверным и не подойдет, если пропатчить образ на устройстве иной модели.
|
||||
|
||||
Приложение Magisk выведет в лог строку, подобную следующей:
|
||||
|
||||
```
|
||||
- Pre-init storage partition device ID: <имя>
|
||||
```
|
||||
|
||||
Также и avbroot может вывести информацию о разделе, обнаруженном Magisk, для этого выполните команду:
|
||||
|
||||
```bash
|
||||
avbroot boot magisk-info \
|
||||
--image magisk_patched-*.img
|
||||
```
|
||||
|
||||
Имя раздела будет выведено как: `PREINITDEVICE=<имя>`.
|
||||
|
||||
Теперь, когда имя раздела известно, его нужно указать avbroot с помощью команды `--magisk-preinit-device <имя>`. Имя раздела стоит запомнить или сохранить где-нибудь на будущее, оно вряд ли изменится при обновлении Magisk.
|
||||
|
||||
Если запустить приложение Magisk на целевом устройстве невозможно (например, телефон не загружается), пропатчите OTA с аргументом `--ignore-magisk-warnings` и прошейте его. Затем выполните указанные выше шаги и повторно пропатчите OTA, но уже с указанием аргумента `--magisk-preinit-device <имя>`.
|
||||
|
||||
## Проверка OTA
|
||||
|
||||
Чтобы проверить все подписи и хэши, связанные с установкой OTA и процессом загрузки AVB, выполните команду:
|
||||
|
||||
```bash
|
||||
avbroot ota verify \
|
||||
--input /путь/к/ota.zip \
|
||||
--cert-ota /путь/к/ota.crt \
|
||||
--public-key-avb /путь/к/avb_pkmd.bin
|
||||
```
|
||||
|
||||
Эта команда работает для любого OTA, независимо от того, пропатчено оно или нет.
|
||||
|
||||
Если опции `--cert-ota` и `--public-key-avb` не указаны, то подписи проверяются только на корректность, не проверяя, совпадают ли они внутри всех файлов.
|
||||
|
||||
## Подсказки через Tab
|
||||
|
||||
Поскольку avbroot имеет множество опций, будет удобно настроить подсказки с автозаполнением для используемой оболочки. Конфигурации генерируются в самом avbroot.
|
||||
|
||||
#### bash
|
||||
|
||||
Добавьте в `~/.bashrc`:
|
||||
|
||||
```bash
|
||||
eval "$(avbroot completion -s bash)"
|
||||
```
|
||||
|
||||
#### zsh
|
||||
|
||||
Добавьте в `~/.zshrc`:
|
||||
|
||||
```bash
|
||||
eval "$(avbroot completion -s zsh)"
|
||||
```
|
||||
|
||||
#### fish
|
||||
|
||||
Добавьте в `~/.config/fish/config.fish`:
|
||||
|
||||
```bash
|
||||
avbroot completion -s fish | source
|
||||
```
|
||||
|
||||
#### PowerShell
|
||||
|
||||
Добавьте в загрузочный скрипт PowerShell (`profile.ps1`):
|
||||
|
||||
```powershell
|
||||
Invoke-Expression (& avbroot completion -s powershell)
|
||||
```
|
||||
|
||||
## Расширенное использование
|
||||
|
||||
### Использование заранее пропатченного boot.img
|
||||
|
||||
avbroot может подменить используемый загрузочный образ на заранее пропатченный (вместо того, чтобы самостоятельно применять патч). Это пригодится в случае, если у вас уже имеется пропатченный через приложение Magisk образ ядра или образ с поддержкой KernelSU. Для этого используйте аргумент `--prepatched <загрузочный образ>` вместо `--magisk <apk>`. То есть, указав `--prepatched`, avbroot пропустит применение патчинга Magisk'ом, но по-прежнему применит патч OTA-сертификата.
|
||||
|
||||
Обратите внимание, что avbroot проверяет совместимость предварительно пропатченного образа с оригинальным. Например, если поля заголовка образа не совпадают, или вовсе указан иной, незагрузочный образ, то процесс патча будет прерван. Эти проверки, конечно, ничего не гарантируют, но должны предостеречь от случайного использования некорректного образа. Чтобы обойти базовые проверки безопасности, укажите аргумент `--ignore-prepatched-compat`. Если вы хотите убрать вообще все проверки (чего делать крайне не рекомендуется), укажите его дважды.
|
||||
|
||||
### Пропуск патчей для root-доступа
|
||||
|
||||
avbroot можно использовать для простого переподписания OTA, указав аргумент `--rootless` вместо `--magisk`/`--prepatched`. В таком случае пропатченный OTA не будет рутирован. Единственная модификация, которая будет применена – это замена сертификата проверки OTA, чтобы систему можно было обновлять с помощью будущих пропатченных OTA.
|
||||
|
||||
### Подмена разделов
|
||||
|
||||
avbroot поддерживает подмену целых образов в OTA, даже тех, что не являются загрузочными (например, `vendor_dlkm`). Образ можно заменить, указав аргумент `--replace <имя раздела> /путь/к/образу.img`.
|
||||
|
||||
Единственное, что меняется – это то, откуда считывается раздел. При использовании `--replace` вместо использования образа раздела из оригинального `payload.bin` в OTA, он берется напрямую по указанному вами пути. Таким образом, заменяющие образы разделов должны иметь правильные колонтитулы vbmeta, соответствующие оригинальным.
|
||||
|
||||
Это не влияет на ход применения пачтей. Например, при использовании Magisk, патч получения root-прав применяется к загрузочному образу одинаково, независимо от того, был ли он получен из оригинального `payload.bin` или это файл, указанный через `--replace`.
|
||||
|
||||
### Очистка флагов vbmeta
|
||||
|
||||
Некоторые сборки Android-прошивок могут поставляться с образом `vbmeta`, в котором флаги установлены таким образом, что AVB фактически отключен. Если avbroot сталкивается с такими образом, процесс патчинга завершается ошибкой с сообщением следующего типа:
|
||||
|
||||
```
|
||||
Verified boot is disabled by vbmeta's header flags: 0x3
|
||||
```
|
||||
|
||||
Чтобы принудительно включить AVB (очистив флаги), укажите аргумент `--clear-vbmeta-flags`.
|
||||
|
||||
### Использование в неинтерактивном режиме
|
||||
|
||||
По умолчанию avbroot интерактивно запрашивает пароли к приватным ключам. Чтобы запустить avbroot в неинтерактивном режиме, можно:
|
||||
|
||||
* Предоставить пароли через файлы:
|
||||
|
||||
```bash
|
||||
avbroot ota patch \
|
||||
--pass-avb-file /путь/к/avb.passphrase \
|
||||
--pass-ota-file /путь/к/ota.passphrase \
|
||||
<...>
|
||||
```
|
||||
|
||||
На Unix-подобных системах "файлы" могут быть каналами ("pipes"). В оболочках, поддерживающих подстановку процесса (bash, zsh и т. д.), пароль можно запросить с помощью команды (например, запрашивая у менеджера паролей).
|
||||
|
||||
```bash
|
||||
avbroot ota patch \
|
||||
--pass-avb-file <(команда для запроса пароля AVB) \
|
||||
--pass-ota-file <(команда для запроса пароля OTA) \
|
||||
<...>
|
||||
```
|
||||
|
||||
* Предоставить пароли через переменные среды. Это менее безопасно, поскольку любой процесс, запущенный от имени того же пользователя, может видеть значения переменных среды.
|
||||
|
||||
```bash
|
||||
export PASSPHRASE_AVB="пароль AVB"
|
||||
export PASSPHRASE_OTA="пароль OTA"
|
||||
|
||||
avbroot ota patch \
|
||||
--pass-avb-env-var PASSPHRASE_AVB \
|
||||
--pass-ota-env-var PASSPHRASE_OTA \
|
||||
<...>
|
||||
```
|
||||
|
||||
* Использовать незашифрованные приватные ключи. Крайне не рекомендуется.
|
||||
|
||||
### Извлечение всей OTA
|
||||
|
||||
Чтобы извлечь все образы, содержащиеся в `payload.bin`, используйте команду:
|
||||
|
||||
```bash
|
||||
avbroot ota extract \
|
||||
--input /путь/к/ota.zip \
|
||||
--directory extracted \
|
||||
--all
|
||||
```
|
||||
|
||||
## Сборка из исходного кода
|
||||
|
||||
Убедитесь, что у вас установлен [набор инструментов Rust.](https://www.rust-lang.org/ru/) Затем выполните:
|
||||
|
||||
```bash
|
||||
cargo build --release
|
||||
```
|
||||
|
||||
Исполняемый файл будет записан в `target/release/avbroot`.
|
||||
|
||||
Дебаг-сборки тоже работают, но они будут работать значительно медленнее (в вычислениях sha256), потому что оптимизации компилятора отключены.
|
||||
|
||||
По умолчанию исполняемый файл ссылается на системные библиотеки bzip2 и liblzma, от которых зависит avbroot. Чтобы скомпилировать и статически связать эти две библиотеки, укажите аргумент `--features static`.
|
||||
|
||||
## Проверка цифровых подписей
|
||||
|
||||
Сначала сохраните открытый ключ в файл, перечисляющий ключи, которым можно доверять. Это тот же ключ, который указан [в профиле автора.](https://github.com/chenxiaolong/)
|
||||
|
||||
```bash
|
||||
echo 'avbroot ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIDOe6/tBnO7xZhAWXRj3ApUYgn+XZ0wnQiXM8B7tPgv4' > avbroot_trusted_keys
|
||||
```
|
||||
|
||||
Затем проверьте подпись zip-файла, используя список доверенных ключей.
|
||||
|
||||
```bash
|
||||
ssh-keygen -Y verify -f avbroot_trusted_keys -I avbroot -n file -s <file>.zip.sig < <file>.zip
|
||||
```
|
||||
|
||||
Если файл успешно проверен, вывод будет следующим:
|
||||
|
||||
```
|
||||
Good "file" signature for avbroot with ED25519 key SHA256:Ct0HoRyrFLrnF9W+A/BKEiJmwx7yWkgaW/JvghKrboA
|
||||
```
|
||||
|
||||
## Вклад
|
||||
|
||||
Буду рад вашему вкладу в разработку! Однако я вряд ли приму изменения для поддержки устройств, которые ведут себя значительно иначе, чем устройства Pixel.
|
||||
|
||||
## Лицензия
|
||||
|
||||
avbroot распространяется по лицензии GPLv3. Полный текст лицензии см. в [`LICENSE`.](./LICENSE)
|
||||
|
||||
+153
-11
@@ -388,7 +388,7 @@ fn get_vbmeta_patch_order(
|
||||
}
|
||||
|
||||
if !missing.is_empty() {
|
||||
warn!("Partitions aren't protected by AVB: {:?}", joined(missing));
|
||||
warn!("Partitions aren't protected by AVB: {}", joined(missing));
|
||||
}
|
||||
|
||||
// Ensure that there's only a single root of trust. Otherwise, there could
|
||||
@@ -493,9 +493,7 @@ fn update_security_descriptors(
|
||||
/// Get the text before the first equal sign in the kernel command line if it is
|
||||
/// not empty.
|
||||
fn cmdline_prefix(cmdline: &str) -> Option<&str> {
|
||||
let Some((prefix, _)) = cmdline.split_once('=') else {
|
||||
return None;
|
||||
};
|
||||
let (prefix, _) = cmdline.split_once('=')?;
|
||||
if prefix.is_empty() {
|
||||
return None;
|
||||
}
|
||||
@@ -916,7 +914,7 @@ fn patch_ota_zip(
|
||||
}
|
||||
|
||||
if !missing.is_empty() {
|
||||
bail!("Missing entries in OTA zip: {:?}", joined(missing));
|
||||
bail!("Missing entries in OTA zip: {}", joined(missing));
|
||||
} else if !paths.contains(ota::PATH_METADATA) && !paths.contains(ota::PATH_METADATA_PB) {
|
||||
bail!(
|
||||
"Neither legacy nor protobuf OTA metadata files exist: {:?}, {:?}",
|
||||
@@ -1346,11 +1344,12 @@ pub fn extract_subcommand(cli: &ExtractCli, cancel_signal: &AtomicBool) -> Resul
|
||||
.with_context(|| format!("Failed to open for reading: {:?}", cli.input))?;
|
||||
let mut zip = ZipArchive::new(BufReader::new(raw_reader.reopen()?))
|
||||
.with_context(|| format!("Failed to read zip: {:?}", cli.input))?;
|
||||
let payload_entry = zip
|
||||
.by_name(ota::PATH_PAYLOAD)
|
||||
.with_context(|| format!("Failed to open zip entry: {:?}", ota::PATH_PAYLOAD))?;
|
||||
let payload_offset = payload_entry.data_start();
|
||||
let payload_size = payload_entry.size();
|
||||
let (payload_offset, payload_size) = {
|
||||
let entry = zip
|
||||
.by_name(ota::PATH_PAYLOAD)
|
||||
.with_context(|| format!("Failed to open zip entry: {}", ota::PATH_PAYLOAD))?;
|
||||
(entry.data_start(), entry.size())
|
||||
};
|
||||
|
||||
// Open the payload data directly.
|
||||
let mut payload_reader = SectionReader::new(
|
||||
@@ -1403,6 +1402,144 @@ pub fn extract_subcommand(cli: &ExtractCli, cancel_signal: &AtomicBool) -> Resul
|
||||
cancel_signal,
|
||||
)?;
|
||||
|
||||
if cli.fastboot {
|
||||
const ANDROID_INFO: &str = "android-info.txt";
|
||||
const FASTBOOT_INFO: &str = "fastboot-info.txt";
|
||||
|
||||
// Generate android-info.txt, which is always required for fastboot's
|
||||
// flashall subcommand. We only add a basic device check to avoid
|
||||
// accidental flashes on the wrong device.
|
||||
|
||||
let mut metadata_entry = zip
|
||||
.by_name(ota::PATH_METADATA_PB)
|
||||
.with_context(|| format!("Failed to open zip entry: {:?}", ota::PATH_METADATA_PB))?;
|
||||
let mut metadata_raw = vec![];
|
||||
metadata_entry
|
||||
.read_to_end(&mut metadata_raw)
|
||||
.with_context(|| format!("Failed to read OTA metadata: {}", ota::PATH_METADATA_PB))?;
|
||||
let metadata = ota::parse_protobuf_metadata(&metadata_raw)
|
||||
.with_context(|| format!("Failed to parse OTA metadata: {}", ota::PATH_METADATA_PB))?;
|
||||
|
||||
let device = metadata
|
||||
.precondition
|
||||
.as_ref()
|
||||
.and_then(|p| p.device.first())
|
||||
.ok_or_else(|| anyhow!("Device codename not found in OTA metadata"))?;
|
||||
|
||||
directory
|
||||
.write(ANDROID_INFO, format!("require board={device}\n"))
|
||||
.with_context(|| format!("Failed to write file: {ANDROID_INFO}"))?;
|
||||
|
||||
// Find out which images can be flashed with fastboot. The bootloader
|
||||
// (and potentially modem) partitions need to be flashed as a whole and
|
||||
// an OTA doesn't contain sufficient information to generate the
|
||||
// required combined file.
|
||||
let mut flashable_images = BTreeSet::new();
|
||||
|
||||
for name in &unique_images {
|
||||
let file = directory
|
||||
.open(format!("{name}.img"))
|
||||
.with_context(|| format!("Failed to open image for reading: {name}"))?;
|
||||
|
||||
match avb::load_image(file) {
|
||||
Ok(_) => {
|
||||
flashable_images.insert(name);
|
||||
}
|
||||
// Treat images without AVB metadata as bootloader partitions.
|
||||
Err(avb::Error::InvalidHeaderMagic(_)) => continue,
|
||||
Err(e) => return Err(e).with_context(|| format!("Failed to load image: {name}")),
|
||||
}
|
||||
}
|
||||
|
||||
// Generate fastboot-info.txt to be able to control how exactly the
|
||||
// images are flashed. This solves two problems:
|
||||
//
|
||||
// 1. fastboot flashall, by default, expects super_empty.img to exist
|
||||
// for A/B devices. If it doesn't exist, it assumes that all images
|
||||
// are meant for non-dynamic partitions and skips the fastbootd
|
||||
// reboot. We cannot generate a proper super_empty.img because it
|
||||
// requires knowing the number of super partitions along with their
|
||||
// names and sizes. This information is not available in an OTA. The
|
||||
// fastboot info file allows us to control when to reboot to
|
||||
// fastbootd and flash each partition.
|
||||
//
|
||||
// This approach is a bit slower because we have to reboot into
|
||||
// fastbootd. When the device only has a single super partition,
|
||||
// fastboot will normally locally combine super_empty.img with the
|
||||
// individual partitions to form a full super image and then flash it
|
||||
// via the bootloader's fastboot mode.
|
||||
//
|
||||
// Also, because we can't generate super_empty.img, if the super
|
||||
// partition metadata on the device somehow becomes corrupted,
|
||||
// running fastboot flash all using avbroot's extracted images isn't
|
||||
// sufficient for recovering the device. The user will need to copy
|
||||
// that file from the factory image into the output directory and add
|
||||
// an `update-super` line after `reboot fastboot` the fastboot info
|
||||
// file.
|
||||
//
|
||||
// 2. fastboot flashall does not look at all .img files in a directory.
|
||||
// Instead, it has a hardcoded list of partitions known to AOSP. This
|
||||
// means obscure OEM-specific partitions are just silently ignored.
|
||||
// Using a fastboot info file with explicit flash instructions avoids
|
||||
// this problem entirely.
|
||||
|
||||
let mut dynamic = BTreeSet::new();
|
||||
|
||||
if let Some(dpm) = &header.manifest.dynamic_partition_metadata {
|
||||
for group in &dpm.groups {
|
||||
for name in &group.partition_names {
|
||||
if unique_images.contains(name) {
|
||||
dynamic.insert(name);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let flash_command = |name| {
|
||||
let (prefix, suffix) = if flashable_images.contains(name) {
|
||||
("", "")
|
||||
} else {
|
||||
("#", " # Bootloader/modem images cannot be flashed")
|
||||
};
|
||||
|
||||
let extra_arg = if *name == "vbmeta" {
|
||||
"--apply-vbmeta "
|
||||
} else {
|
||||
""
|
||||
};
|
||||
|
||||
format!("{prefix}flash {extra_arg}{name}{suffix}\n")
|
||||
};
|
||||
|
||||
let mut fastboot_info = String::new();
|
||||
|
||||
fastboot_info.push_str("# Generated by avbroot\n");
|
||||
fastboot_info.push_str("version 1\n");
|
||||
|
||||
fastboot_info.push_str("# Flash non-dynamic partitions\n");
|
||||
for name in &unique_images {
|
||||
if !dynamic.contains(name) {
|
||||
fastboot_info.push_str(&flash_command(name));
|
||||
}
|
||||
}
|
||||
|
||||
fastboot_info.push_str("# Reboot to fastbootd\n");
|
||||
fastboot_info.push_str("reboot fastboot\n");
|
||||
|
||||
fastboot_info.push_str("# Flash dynamic partitions\n");
|
||||
for name in &dynamic {
|
||||
fastboot_info.push_str(&flash_command(name));
|
||||
}
|
||||
|
||||
fastboot_info.push_str("# Wipe data when -w flag is used\n");
|
||||
fastboot_info.push_str("if-wipe erase userdata\n");
|
||||
fastboot_info.push_str("if-wipe erase metadata\n");
|
||||
|
||||
directory
|
||||
.write(FASTBOOT_INFO, fastboot_info)
|
||||
.with_context(|| format!("Failed to write file: {FASTBOOT_INFO}"))?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1712,6 +1849,7 @@ pub struct PatchCli {
|
||||
#[arg(
|
||||
long,
|
||||
value_name = "PARTITION",
|
||||
hide = true,
|
||||
help_heading = HEADING_OTHER
|
||||
)]
|
||||
pub boot_partition: Option<String>,
|
||||
@@ -1737,8 +1875,12 @@ pub struct ExtractCli {
|
||||
pub boot_only: bool,
|
||||
|
||||
/// (Deprecated: no longer needed)
|
||||
#[arg(long, value_name = "PARTITION")]
|
||||
#[arg(long, value_name = "PARTITION", hide = true)]
|
||||
pub boot_partition: Option<String>,
|
||||
|
||||
/// Generate fastboot info files.
|
||||
#[arg(long)]
|
||||
pub fastboot: bool,
|
||||
}
|
||||
|
||||
/// Verify signatures of an OTA.
|
||||
|
||||
@@ -164,6 +164,9 @@ fn reformat_pem(data: &[u8]) -> Result<Vec<u8>> {
|
||||
continue;
|
||||
} else if line.starts_with(b"-----BEGIN CERTIFICATE-----") {
|
||||
inside_base64 = true;
|
||||
|
||||
result.extend_from_slice(line);
|
||||
result.push(b'\n');
|
||||
} else if line.starts_with(b"-----END CERTIFICATE-----") {
|
||||
inside_base64 = false;
|
||||
|
||||
@@ -173,13 +176,13 @@ fn reformat_pem(data: &[u8]) -> Result<Vec<u8>> {
|
||||
}
|
||||
|
||||
base64.clear();
|
||||
|
||||
result.extend_from_slice(line);
|
||||
result.push(b'\n');
|
||||
} else if inside_base64 {
|
||||
base64.extend_from_slice(line);
|
||||
continue;
|
||||
}
|
||||
|
||||
result.extend_from_slice(line);
|
||||
result.push(b'\n');
|
||||
}
|
||||
|
||||
if inside_base64 {
|
||||
|
||||
@@ -52,18 +52,19 @@ pub enum Error {
|
||||
|
||||
type Result<T> = std::result::Result<T, Error>;
|
||||
|
||||
pub struct HashTree<'a> {
|
||||
pub struct HashTree {
|
||||
block_size: u32,
|
||||
algorithm: &'static Algorithm,
|
||||
salt: &'a [u8],
|
||||
salted_context: Context,
|
||||
}
|
||||
|
||||
impl<'a> HashTree<'a> {
|
||||
pub fn new(block_size: u32, algorithm: &'static Algorithm, salt: &'a [u8]) -> Self {
|
||||
impl HashTree {
|
||||
pub fn new(block_size: u32, algorithm: &'static Algorithm, salt: &[u8]) -> Self {
|
||||
let mut salted_context = Context::new(algorithm);
|
||||
salted_context.update(salt);
|
||||
|
||||
Self {
|
||||
block_size,
|
||||
algorithm,
|
||||
salt,
|
||||
salted_context,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -72,7 +73,8 @@ impl<'a> HashTree<'a> {
|
||||
/// in the list. Note that the bottom level is stored at the end of the hash
|
||||
/// tree data.
|
||||
fn compute_level_offsets(&self, image_size: u64) -> Result<Vec<Range<usize>>> {
|
||||
let digest_size = self.algorithm.output_len().next_power_of_two();
|
||||
let algorithm = self.salted_context.algorithm();
|
||||
let digest_size = algorithm.output_len().next_power_of_two();
|
||||
let mut ranges = vec![];
|
||||
let mut level_size = image_size;
|
||||
|
||||
@@ -143,8 +145,8 @@ impl<'a> HashTree<'a> {
|
||||
cancel_signal: &AtomicBool,
|
||||
) -> io::Result<()> {
|
||||
// Each digest must be a power of 2.
|
||||
let digest_padding =
|
||||
self.algorithm.output_len().next_power_of_two() - self.algorithm.output_len();
|
||||
let algorithm = self.salted_context.algorithm();
|
||||
let digest_padding = algorithm.output_len().next_power_of_two() - algorithm.output_len();
|
||||
let mut buf = vec![0u8; self.block_size as usize];
|
||||
|
||||
while size > 0 {
|
||||
@@ -157,8 +159,7 @@ impl<'a> HashTree<'a> {
|
||||
// with padding.
|
||||
buf[n..].fill(0);
|
||||
|
||||
let mut context = Context::new(self.algorithm);
|
||||
context.update(self.salt);
|
||||
let mut context = self.salted_context.clone();
|
||||
context.update(&buf);
|
||||
|
||||
// Add the digest to the tree level. Each tree node must be a power
|
||||
@@ -191,7 +192,8 @@ impl<'a> HashTree<'a> {
|
||||
);
|
||||
|
||||
// Parallelize in larger chunks to avoid too much seek thrashing.
|
||||
let digest_size = self.algorithm.output_len().next_power_of_two();
|
||||
let algorithm = self.salted_context.algorithm();
|
||||
let digest_size = algorithm.output_len().next_power_of_two();
|
||||
let multiplier = 1024u64;
|
||||
|
||||
level_data
|
||||
@@ -222,7 +224,8 @@ impl<'a> HashTree<'a> {
|
||||
level_data: &mut [u8],
|
||||
cancel_signal: &AtomicBool,
|
||||
) -> io::Result<()> {
|
||||
let digest_size = self.algorithm.output_len().next_power_of_two();
|
||||
let algorithm = self.salted_context.algorithm();
|
||||
let digest_size = algorithm.output_len().next_power_of_two();
|
||||
|
||||
level_data
|
||||
.par_chunks_exact_mut(digest_size)
|
||||
@@ -263,8 +266,7 @@ impl<'a> HashTree<'a> {
|
||||
let mut buf = vec![0u8; image_size as usize];
|
||||
reader.read_exact(&mut buf)?;
|
||||
|
||||
let mut context = Context::new(self.algorithm);
|
||||
context.update(self.salt);
|
||||
let mut context = self.salted_context.clone();
|
||||
context.update(&buf);
|
||||
let digest = context.finish();
|
||||
|
||||
@@ -309,8 +311,7 @@ impl<'a> HashTree<'a> {
|
||||
}
|
||||
|
||||
// Calculate the root hash.
|
||||
let mut context = Context::new(self.algorithm);
|
||||
context.update(self.salt);
|
||||
let mut context = self.salted_context.clone();
|
||||
context.update(&hash_tree_data[level_offsets.last().unwrap().clone()]);
|
||||
let root_hash = context.finish().as_ref().to_vec();
|
||||
|
||||
@@ -402,8 +403,9 @@ impl<'a> HashTree<'a> {
|
||||
|
||||
if hash_tree_data != actual_hash_tree_data {
|
||||
// These are multiple megabytes, so only report the hashes.
|
||||
let expected = ring::digest::digest(self.algorithm, hash_tree_data);
|
||||
let actual = ring::digest::digest(self.algorithm, &actual_hash_tree_data);
|
||||
let algorithm = self.salted_context.algorithm();
|
||||
let expected = ring::digest::digest(algorithm, hash_tree_data);
|
||||
let actual = ring::digest::digest(algorithm, &actual_hash_tree_data);
|
||||
|
||||
return Err(Error::InvalidHashTree {
|
||||
expected: hex::encode(expected),
|
||||
|
||||
+283
-105
@@ -1,10 +1,10 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2023 Andrew Gunnerson
|
||||
* SPDX-FileCopyrightText: 2023-2024 Andrew Gunnerson
|
||||
* SPDX-License-Identifier: GPL-3.0-only
|
||||
*/
|
||||
|
||||
use std::{
|
||||
io::{Cursor, Read, Seek, SeekFrom, Write},
|
||||
io::{Cursor, Read, Seek, Write},
|
||||
sync::atomic::AtomicBool,
|
||||
};
|
||||
|
||||
@@ -14,8 +14,12 @@ use rsa::RsaPrivateKey;
|
||||
|
||||
use avbroot::{
|
||||
self,
|
||||
format::avb::{self, AppendedDescriptorMut, AppendedDescriptorRef},
|
||||
stream::{self, SharedCursor},
|
||||
format::avb::{
|
||||
self, AlgorithmType, AppendedDescriptorMut, AppendedDescriptorRef,
|
||||
ChainPartitionDescriptor, Descriptor, Footer, HashDescriptor, HashTreeDescriptor, Header,
|
||||
KernelCmdlineDescriptor, PropertyDescriptor,
|
||||
},
|
||||
stream::SharedCursor,
|
||||
};
|
||||
|
||||
fn get_test_key() -> RsaPrivateKey {
|
||||
@@ -31,154 +35,328 @@ fn get_test_key() -> RsaPrivateKey {
|
||||
RsaPrivateKey::from_pkcs8_encrypted_pem(data, passphrase.trim_end()).unwrap()
|
||||
}
|
||||
|
||||
fn repeat_str(s: &str, max_len: usize) -> String {
|
||||
assert!(!s.is_empty());
|
||||
|
||||
let mut result = s.repeat(max_len / s.len());
|
||||
result.push_str(&s[..max_len % s.len()]);
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
fn repeat_array<const N: usize>(data: &[u8]) -> [u8; N] {
|
||||
assert!(!data.is_empty());
|
||||
|
||||
let mut result = [0u8; N];
|
||||
|
||||
for i in 0..N / data.len() {
|
||||
result[i * data.len()..][..data.len()].copy_from_slice(data);
|
||||
}
|
||||
|
||||
let remain = N % data.len();
|
||||
result[N - remain..].copy_from_slice(&data[..remain]);
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn round_trip_root_image() {
|
||||
let data = include_bytes!(concat!(
|
||||
env!("CARGO_MANIFEST_DIR"),
|
||||
"/tests/data/vbmeta_root.img",
|
||||
));
|
||||
let reader = Cursor::new(data);
|
||||
|
||||
let (mut header, footer, _) = avb::load_image(reader).unwrap();
|
||||
assert_matches!(footer, None);
|
||||
let mut header = Header {
|
||||
required_libavb_version_major: 1,
|
||||
required_libavb_version_minor: 0,
|
||||
algorithm_type: AlgorithmType::Sha256Rsa4096,
|
||||
hash: vec![], // autogenerated
|
||||
signature: vec![], // autogenerated
|
||||
public_key: vec![], // autogenerated
|
||||
public_key_metadata: vec![
|
||||
0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d,
|
||||
0x0e, 0x0f,
|
||||
],
|
||||
descriptors: vec![
|
||||
Descriptor::Property(PropertyDescriptor {
|
||||
key: "foobar".to_owned(),
|
||||
value: b"Invalid UTF-8: \xFF".to_vec(),
|
||||
}),
|
||||
Descriptor::HashTree(HashTreeDescriptor {
|
||||
dm_verity_version: 1,
|
||||
image_size: 4096,
|
||||
tree_offset: 0,
|
||||
tree_size: 2048,
|
||||
data_block_size: 4096,
|
||||
hash_block_size: 4096,
|
||||
fec_num_roots: 1,
|
||||
fec_offset: 2048,
|
||||
fec_size: 2048,
|
||||
hash_algorithm: "sha512".to_owned(),
|
||||
partition_name: "hashtreed_partition".to_owned(),
|
||||
salt: [0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef].repeat(8),
|
||||
root_digest: [0xfe, 0xdc, 0xba, 0x98, 0x76, 0x54, 0x32, 0x10].repeat(8),
|
||||
flags: 0,
|
||||
reserved: repeat_array(&[0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef]),
|
||||
}),
|
||||
Descriptor::Hash(HashDescriptor {
|
||||
image_size: 6,
|
||||
hash_algorithm: "sha256".to_owned(),
|
||||
partition_name: "hashed_partition".to_owned(),
|
||||
salt: [0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef].repeat(4),
|
||||
root_digest: [0xfe, 0xdc, 0xba, 0x98, 0x76, 0x54, 0x32, 0x10].repeat(4),
|
||||
flags: 0,
|
||||
reserved: repeat_array(&[0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef]),
|
||||
}),
|
||||
Descriptor::KernelCmdline(KernelCmdlineDescriptor {
|
||||
flags: 1,
|
||||
cmdline: "foobar".to_owned(),
|
||||
}),
|
||||
Descriptor::ChainPartition(ChainPartitionDescriptor {
|
||||
rollback_index_location: 1,
|
||||
partition_name: "chained_partition".to_owned(),
|
||||
public_key: [0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef].repeat(129),
|
||||
flags: 0xfedcba98,
|
||||
reserved: repeat_array(&[0x76, 0x54, 0x32, 0x10, 0xfe, 0xdc, 0xba, 0x98]),
|
||||
}),
|
||||
],
|
||||
rollback_index: 1677974400,
|
||||
flags: 0,
|
||||
rollback_index_location: 0,
|
||||
release_string: repeat_str("MaxLength", 48),
|
||||
reserved: repeat_array(&[0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef]),
|
||||
};
|
||||
|
||||
// Sign the header.
|
||||
let key = get_test_key();
|
||||
|
||||
header.sign(&key).unwrap();
|
||||
assert_eq!(header.verify().unwrap().unwrap(), key.to_public_key());
|
||||
|
||||
// Clear out the signature-related fields and re-sign.
|
||||
header.hash.clear();
|
||||
header.signature.clear();
|
||||
header.public_key.clear();
|
||||
header.sign(&key).unwrap();
|
||||
|
||||
// Write vbmeta structures.
|
||||
let mut writer = Cursor::new(Vec::new());
|
||||
avb::write_root_image(&mut writer, &header, 64).unwrap();
|
||||
let new_data = writer.into_inner();
|
||||
let data = writer.into_inner();
|
||||
|
||||
assert_eq!(data, new_data.as_slice());
|
||||
// Verify checksum of the output.
|
||||
assert_eq!(
|
||||
ring::digest::digest(&ring::digest::SHA512, &data).as_ref(),
|
||||
[
|
||||
0xc4, 0xa5, 0xda, 0x3e, 0x09, 0xa2, 0xc8, 0x70, 0xcb, 0xf0, 0x96, 0x79, 0x0e, 0x1e,
|
||||
0x80, 0xae, 0x5e, 0x37, 0x81, 0x27, 0x24, 0xc3, 0x6c, 0xa9, 0x42, 0x9e, 0x2c, 0xb1,
|
||||
0x81, 0xad, 0xce, 0xee, 0x8d, 0x4f, 0x76, 0x45, 0x54, 0xc1, 0x31, 0x6a, 0xa7, 0x81,
|
||||
0x5c, 0x59, 0xa8, 0xe8, 0x76, 0xab, 0xed, 0x5b, 0x07, 0x07, 0x38, 0xdd, 0x09, 0x86,
|
||||
0x05, 0x39, 0x23, 0x2d, 0x7b, 0xcc, 0x57, 0x06,
|
||||
],
|
||||
);
|
||||
|
||||
// Parse the generated image.
|
||||
let mut reader = Cursor::new(&data);
|
||||
let (new_header, new_footer, new_image_size) = avb::load_image(&mut reader).unwrap();
|
||||
assert_matches!(new_footer, None);
|
||||
|
||||
assert_eq!(new_header, header);
|
||||
assert_eq!(new_image_size, data.len() as u64);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn round_trip_appended_hash_image() {
|
||||
let data = include_bytes!(concat!(
|
||||
env!("CARGO_MANIFEST_DIR"),
|
||||
"/tests/data/vbmeta_appended_hash.img",
|
||||
));
|
||||
let mut reader = Cursor::new(data);
|
||||
let cancel_signal = AtomicBool::new(false);
|
||||
|
||||
let (mut header, footer, image_size) = avb::load_image(&mut reader).unwrap();
|
||||
let mut footer = footer.unwrap();
|
||||
|
||||
let key = get_test_key();
|
||||
|
||||
assert_eq!(header.verify().unwrap().unwrap(), key.to_public_key());
|
||||
|
||||
// Verify the digest.
|
||||
match header.appended_descriptor().unwrap() {
|
||||
AppendedDescriptorRef::HashTree(_) => panic!("Expected hash descriptor"),
|
||||
AppendedDescriptorRef::Hash(d) => {
|
||||
reader.rewind().unwrap();
|
||||
d.verify(&mut reader, &cancel_signal).unwrap();
|
||||
}
|
||||
}
|
||||
let image_size = 12288;
|
||||
let raw_data = b"foobar";
|
||||
let mut header = Header {
|
||||
required_libavb_version_major: 1,
|
||||
required_libavb_version_minor: 0,
|
||||
algorithm_type: AlgorithmType::Sha256Rsa4096,
|
||||
hash: vec![], // autogenerated
|
||||
signature: vec![], // autogenerated
|
||||
public_key: vec![], // autogenerated
|
||||
public_key_metadata: vec![
|
||||
0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d,
|
||||
0x0e, 0x0f,
|
||||
],
|
||||
descriptors: vec![
|
||||
Descriptor::Property(PropertyDescriptor {
|
||||
key: "foobar".to_owned(),
|
||||
value: b"Invalid UTF-8: \xFF".to_vec(),
|
||||
}),
|
||||
Descriptor::Hash(HashDescriptor {
|
||||
image_size: raw_data.len() as u64,
|
||||
hash_algorithm: "sha256".to_owned(),
|
||||
partition_name: "vbmeta_appended_hash".to_owned(),
|
||||
salt: [0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef].repeat(4),
|
||||
root_digest: vec![], // autogenerated
|
||||
flags: 0,
|
||||
reserved: repeat_array(&[0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef]),
|
||||
}),
|
||||
],
|
||||
rollback_index: 1677974400,
|
||||
flags: 0,
|
||||
rollback_index_location: 0,
|
||||
release_string: repeat_str("MaxLength", 48),
|
||||
reserved: repeat_array(&[0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef]),
|
||||
};
|
||||
let mut footer = Footer {
|
||||
version_major: 1,
|
||||
version_minor: 0,
|
||||
original_image_size: 0, // autogenerated
|
||||
vbmeta_offset: 0, // autogenerated
|
||||
vbmeta_size: 0, // autogenerated
|
||||
reserved: repeat_array(&[0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef]),
|
||||
};
|
||||
|
||||
let mut writer = Cursor::new(Vec::new());
|
||||
let cancel_signal = AtomicBool::new(false);
|
||||
|
||||
// Copy the partition data.
|
||||
reader.seek(SeekFrom::Start(0)).unwrap();
|
||||
stream::copy_n(
|
||||
&mut reader,
|
||||
&mut writer,
|
||||
footer.original_image_size,
|
||||
&cancel_signal,
|
||||
)
|
||||
.unwrap();
|
||||
// Write the raw partition data.
|
||||
writer.write_all(raw_data).unwrap();
|
||||
|
||||
// Regenerate the digest.
|
||||
// Regenerate the raw image digest.
|
||||
match header.appended_descriptor_mut().unwrap() {
|
||||
AppendedDescriptorMut::HashTree(_) => panic!("Expected hash descriptor"),
|
||||
AppendedDescriptorMut::Hash(d) => {
|
||||
d.root_digest.clear();
|
||||
writer.rewind().unwrap();
|
||||
d.update(&mut writer, &cancel_signal).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
// Clear out the signature-related fields and re-sign.
|
||||
header.hash.clear();
|
||||
header.signature.clear();
|
||||
header.public_key.clear();
|
||||
// Verify the raw image digest.
|
||||
match header.appended_descriptor().unwrap() {
|
||||
AppendedDescriptorRef::HashTree(_) => panic!("Expected hash descriptor"),
|
||||
AppendedDescriptorRef::Hash(d) => {
|
||||
writer.rewind().unwrap();
|
||||
d.verify(&mut writer, &cancel_signal).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
// Sign the header.
|
||||
let key = get_test_key();
|
||||
header.sign(&key).unwrap();
|
||||
assert_eq!(header.verify().unwrap().unwrap(), key.to_public_key());
|
||||
|
||||
// Write new vbmeta structures.
|
||||
// Write vbmeta structures.
|
||||
avb::write_appended_image(&mut writer, &header, &mut footer, image_size).unwrap();
|
||||
let new_data = writer.into_inner();
|
||||
let data = writer.into_inner();
|
||||
|
||||
assert_eq!(data, new_data.as_slice());
|
||||
// Verify checksum of the output.
|
||||
assert_eq!(
|
||||
ring::digest::digest(&ring::digest::SHA512, &data).as_ref(),
|
||||
[
|
||||
0x09, 0x98, 0x0c, 0x9d, 0x11, 0x50, 0xde, 0xb1, 0x55, 0x3b, 0x00, 0x76, 0xbe, 0x25,
|
||||
0xfd, 0xe6, 0x46, 0x22, 0xbd, 0x9a, 0x05, 0x86, 0xea, 0x07, 0x4d, 0x8f, 0x7b, 0x15,
|
||||
0x36, 0x20, 0x0d, 0xf0, 0x7e, 0x96, 0xd2, 0x58, 0xde, 0xf2, 0xa6, 0x91, 0x6d, 0x01,
|
||||
0x7b, 0x03, 0x96, 0x70, 0xf8, 0x3b, 0x76, 0x74, 0xf0, 0xbf, 0x47, 0xe0, 0xd2, 0xd4,
|
||||
0x5d, 0xbf, 0xb7, 0x9c, 0xf5, 0xf8, 0xaf, 0x3c,
|
||||
],
|
||||
);
|
||||
|
||||
// Parse the generated image.
|
||||
let mut reader = Cursor::new(&data);
|
||||
let (new_header, new_footer, new_image_size) = avb::load_image(&mut reader).unwrap();
|
||||
let new_footer = new_footer.unwrap();
|
||||
|
||||
assert_eq!(new_header, header);
|
||||
assert_eq!(new_footer, footer);
|
||||
assert_eq!(new_image_size, image_size);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn round_trip_appended_hash_tree_image() {
|
||||
let data = include_bytes!(concat!(
|
||||
env!("CARGO_MANIFEST_DIR"),
|
||||
"/tests/data/vbmeta_appended_hash_tree.img",
|
||||
));
|
||||
let mut reader = SharedCursor::default();
|
||||
reader.write_all(data).unwrap();
|
||||
let cancel_signal = AtomicBool::new(false);
|
||||
|
||||
let (mut header, footer, image_size) = avb::load_image(&mut reader).unwrap();
|
||||
let mut footer = footer.unwrap();
|
||||
|
||||
let key = get_test_key();
|
||||
|
||||
assert_eq!(header.verify().unwrap().unwrap(), key.to_public_key());
|
||||
|
||||
// Verify the hash tree and FEC data.
|
||||
match header.appended_descriptor().unwrap() {
|
||||
AppendedDescriptorRef::HashTree(d) => d.verify(&reader, &cancel_signal).unwrap(),
|
||||
AppendedDescriptorRef::Hash(_) => panic!("Expected hash tree descriptor"),
|
||||
}
|
||||
let image_size = 28672;
|
||||
let raw_data: [u8; 8192] = repeat_array(b"foobar");
|
||||
let mut header = Header {
|
||||
required_libavb_version_major: 1,
|
||||
required_libavb_version_minor: 0,
|
||||
algorithm_type: AlgorithmType::Sha256Rsa4096,
|
||||
hash: vec![], // autogenerated
|
||||
signature: vec![], // autogenerated
|
||||
public_key: vec![], // autogenerated
|
||||
public_key_metadata: vec![
|
||||
0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d,
|
||||
0x0e, 0x0f,
|
||||
],
|
||||
descriptors: vec![
|
||||
Descriptor::Property(PropertyDescriptor {
|
||||
key: "foobar".to_owned(),
|
||||
value: b"Invalid UTF-8: \xFF".to_vec(),
|
||||
}),
|
||||
Descriptor::HashTree(HashTreeDescriptor {
|
||||
dm_verity_version: 1,
|
||||
image_size: raw_data.len() as u64,
|
||||
tree_offset: 0, // autogenerated
|
||||
tree_size: 0, // autogenerated
|
||||
data_block_size: 4096,
|
||||
hash_block_size: 4096,
|
||||
fec_num_roots: 2,
|
||||
fec_offset: 0, // autogenerated
|
||||
fec_size: 0, // autogenerated
|
||||
hash_algorithm: "sha256".to_owned(),
|
||||
partition_name: "vbmeta_appended_hash_tree".to_owned(),
|
||||
salt: [0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef].repeat(4),
|
||||
root_digest: vec![], // autogenerated
|
||||
flags: 0,
|
||||
reserved: repeat_array(&[0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef]),
|
||||
}),
|
||||
],
|
||||
rollback_index: 1677974400,
|
||||
flags: 0,
|
||||
rollback_index_location: 0,
|
||||
release_string: repeat_str("MaxLength", 48),
|
||||
reserved: repeat_array(&[0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef]),
|
||||
};
|
||||
let mut footer = Footer {
|
||||
version_major: 1,
|
||||
version_minor: 0,
|
||||
original_image_size: 0, // autogenerated
|
||||
vbmeta_offset: 0, // autogenerated
|
||||
vbmeta_size: 0, // autogenerated
|
||||
reserved: repeat_array(&[0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef]),
|
||||
};
|
||||
|
||||
let mut writer = SharedCursor::default();
|
||||
let cancel_signal = AtomicBool::new(false);
|
||||
|
||||
// Copy the partition data, excluding the hash tree and FEC data.
|
||||
reader.seek(SeekFrom::Start(0)).unwrap();
|
||||
stream::copy_n(
|
||||
&mut reader,
|
||||
&mut writer,
|
||||
footer.original_image_size,
|
||||
&cancel_signal,
|
||||
)
|
||||
.unwrap();
|
||||
// Write the raw partition data.
|
||||
writer.write_all(&raw_data).unwrap();
|
||||
|
||||
// Regenerate the hash tree and FEC data.
|
||||
// Generate and write the hash tree and FEC data.
|
||||
match header.appended_descriptor_mut().unwrap() {
|
||||
AppendedDescriptorMut::HashTree(d) => {
|
||||
d.root_digest.clear();
|
||||
d.tree_offset = 0;
|
||||
d.tree_size = 0;
|
||||
d.fec_offset = 0;
|
||||
d.fec_size = 0;
|
||||
|
||||
d.update(&writer, &writer, None, &cancel_signal).unwrap();
|
||||
}
|
||||
AppendedDescriptorMut::Hash(_) => panic!("Expected hash tree descriptor"),
|
||||
}
|
||||
|
||||
// Clear out the signature-related fields and re-sign.
|
||||
header.hash.clear();
|
||||
header.signature.clear();
|
||||
header.public_key.clear();
|
||||
// Verify the hash tree and FEC data.
|
||||
match header.appended_descriptor_mut().unwrap() {
|
||||
AppendedDescriptorMut::HashTree(d) => {
|
||||
d.verify(&writer, &cancel_signal).unwrap();
|
||||
}
|
||||
AppendedDescriptorMut::Hash(_) => panic!("Expected hash tree descriptor"),
|
||||
}
|
||||
|
||||
// Sign the header.
|
||||
let key = get_test_key();
|
||||
header.sign(&key).unwrap();
|
||||
assert_eq!(header.verify().unwrap().unwrap(), key.to_public_key());
|
||||
|
||||
// Write new vbmeta structures.
|
||||
// Write vbmeta structures.
|
||||
avb::write_appended_image(&mut writer, &header, &mut footer, image_size).unwrap();
|
||||
let mut new_data = Vec::new();
|
||||
let mut data = Vec::new();
|
||||
writer.rewind().unwrap();
|
||||
writer.read_to_end(&mut new_data).unwrap();
|
||||
writer.read_to_end(&mut data).unwrap();
|
||||
|
||||
assert_eq!(data, new_data.as_slice());
|
||||
// Verify checksum of the output.
|
||||
assert_eq!(
|
||||
ring::digest::digest(&ring::digest::SHA512, &data).as_ref(),
|
||||
[
|
||||
0xd6, 0x69, 0x19, 0x6a, 0x36, 0xc8, 0x1c, 0xe9, 0xc4, 0x85, 0xbe, 0xff, 0x43, 0xb1,
|
||||
0x9f, 0xd4, 0x1d, 0x6c, 0xf9, 0xd2, 0xf3, 0xa6, 0x5f, 0x66, 0x41, 0xd5, 0xf3, 0xfd,
|
||||
0x28, 0xdb, 0x14, 0x67, 0xc6, 0xa8, 0xef, 0xc4, 0xd4, 0x67, 0x6c, 0xb8, 0x66, 0xbb,
|
||||
0x56, 0x5a, 0x4a, 0xf5, 0xd8, 0x92, 0x7c, 0x42, 0xbc, 0x47, 0xdb, 0x94, 0x38, 0x15,
|
||||
0x4b, 0x2d, 0xd0, 0x28, 0x1f, 0xd1, 0x45, 0xa9,
|
||||
],
|
||||
);
|
||||
|
||||
// Parse the generated image.
|
||||
let mut reader = Cursor::new(&data);
|
||||
let (new_header, new_footer, new_image_size) = avb::load_image(&mut reader).unwrap();
|
||||
let new_footer = new_footer.unwrap();
|
||||
|
||||
assert_eq!(new_header, header);
|
||||
assert_eq!(new_footer, footer);
|
||||
assert_eq!(new_image_size, image_size);
|
||||
}
|
||||
|
||||
+284
-60
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2023 Andrew Gunnerson
|
||||
* SPDX-FileCopyrightText: 2023-2024 Andrew Gunnerson
|
||||
* SPDX-License-Identifier: GPL-3.0-only
|
||||
*/
|
||||
|
||||
@@ -7,7 +7,13 @@ use std::io::Cursor;
|
||||
|
||||
use avbroot::{
|
||||
self,
|
||||
format::bootimage::{BootImage, BootImageExt},
|
||||
format::{
|
||||
avb::{AlgorithmType, Descriptor, HashDescriptor, Header},
|
||||
bootimage::{
|
||||
self, BootImage, BootImageExt, BootImageV0Through2, BootImageV3Through4, RamdiskMeta,
|
||||
V1Extra, V2Extra, V4Extra, VendorBootImageV3Through4, VendorV4Extra,
|
||||
},
|
||||
},
|
||||
stream::{FromReader, ToWriter},
|
||||
};
|
||||
use pkcs8::DecodePrivateKey;
|
||||
@@ -26,100 +32,318 @@ fn get_test_key() -> RsaPrivateKey {
|
||||
RsaPrivateKey::from_pkcs8_encrypted_pem(data, passphrase.trim_end()).unwrap()
|
||||
}
|
||||
|
||||
fn round_trip(data: &[u8], expected_version: u32) {
|
||||
let reader = Cursor::new(data);
|
||||
let mut image = BootImage::from_reader(reader).unwrap();
|
||||
fn repeat(s: &str, max_len: usize) -> String {
|
||||
assert!(!s.is_empty());
|
||||
|
||||
let mut result = s.repeat(max_len / s.len());
|
||||
result.push_str(&s[..max_len % s.len()]);
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
fn round_trip(image: &BootImage, sha512: &[u8; 64], expected_version: u32) {
|
||||
assert_eq!(image.header_version(), expected_version);
|
||||
|
||||
match &mut image {
|
||||
BootImage::V3Through4(b) => {
|
||||
let should_sign = b
|
||||
.v4_extra
|
||||
.as_ref()
|
||||
.map_or(false, |v4| v4.signature.is_some());
|
||||
let key = get_test_key();
|
||||
let signed = b.sign(&key).unwrap();
|
||||
assert_eq!(signed, should_sign);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
let mut writer = Cursor::new(Vec::new());
|
||||
image.to_writer(&mut writer).unwrap();
|
||||
let new_data = writer.into_inner();
|
||||
let data = writer.into_inner();
|
||||
|
||||
assert_eq!(data, new_data);
|
||||
assert_eq!(
|
||||
ring::digest::digest(&ring::digest::SHA512, &data).as_ref(),
|
||||
sha512,
|
||||
);
|
||||
|
||||
let reader = Cursor::new(data);
|
||||
let new_image = BootImage::from_reader(reader).unwrap();
|
||||
|
||||
assert_eq!(&new_image, image);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn round_trip_v0() {
|
||||
let data = include_bytes!(concat!(
|
||||
env!("CARGO_MANIFEST_DIR"),
|
||||
"/tests/data/boot_v0.img",
|
||||
));
|
||||
round_trip(data, 0);
|
||||
let image = BootImage::V0Through2(BootImageV0Through2 {
|
||||
kernel_addr: 0x01234567,
|
||||
ramdisk_addr: 0x89abcdef,
|
||||
second_addr: 0x02468ace,
|
||||
tags_addr: 0x13579bdf,
|
||||
page_size: 4096,
|
||||
os_version: 0x76543210,
|
||||
name: repeat("Name", 16),
|
||||
cmdline: repeat("Cmdline", 512),
|
||||
id: [
|
||||
0x00112233, 0x44556677, 0x8899aabb, 0xccddeeff, 0xffeeddcc, 0xbbaa9988, 0x77665544,
|
||||
0x33221100,
|
||||
],
|
||||
extra_cmdline: repeat("ExtraCmdline", 1024),
|
||||
kernel: b"kernel data".to_vec(),
|
||||
ramdisk: b"ramdisk data".to_vec(),
|
||||
second: b"second data".to_vec(),
|
||||
v1_extra: None,
|
||||
v2_extra: None,
|
||||
});
|
||||
let sha512 = [
|
||||
0x23, 0x65, 0x0b, 0xfa, 0x7a, 0x09, 0x0a, 0xdf, 0xdd, 0x9a, 0x6c, 0x03, 0xfa, 0xc5, 0xe1,
|
||||
0xfa, 0x27, 0x65, 0xa0, 0x94, 0xef, 0xa2, 0x0c, 0xc5, 0x3e, 0xd9, 0x67, 0x7d, 0x88, 0x7b,
|
||||
0xb3, 0x48, 0x39, 0xab, 0x28, 0x77, 0x7b, 0x18, 0xec, 0x60, 0xe0, 0xb7, 0x0d, 0x15, 0x26,
|
||||
0xb2, 0xd4, 0x27, 0x25, 0x92, 0x5c, 0x7b, 0x0b, 0x5c, 0xf7, 0xed, 0x27, 0x6c, 0x39, 0xb5,
|
||||
0xb7, 0x44, 0xbb, 0xec,
|
||||
];
|
||||
|
||||
round_trip(&image, &sha512, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn round_trip_v1() {
|
||||
let data = include_bytes!(concat!(
|
||||
env!("CARGO_MANIFEST_DIR"),
|
||||
"/tests/data/boot_v1.img",
|
||||
));
|
||||
round_trip(data, 1);
|
||||
let image = BootImage::V0Through2(BootImageV0Through2 {
|
||||
kernel_addr: 0x01234567,
|
||||
ramdisk_addr: 0x89abcdef,
|
||||
second_addr: 0x02468ace,
|
||||
tags_addr: 0x13579bdf,
|
||||
page_size: 4096,
|
||||
os_version: 0x76543210,
|
||||
name: repeat("Name", 16),
|
||||
cmdline: repeat("Cmdline", 512),
|
||||
id: [
|
||||
0x00112233, 0x44556677, 0x8899aabb, 0xccddeeff, 0xffeeddcc, 0xbbaa9988, 0x77665544,
|
||||
0x33221100,
|
||||
],
|
||||
extra_cmdline: repeat("ExtraCmdline", 1024),
|
||||
kernel: b"kernel data".to_vec(),
|
||||
ramdisk: b"ramdisk data".to_vec(),
|
||||
second: b"second data".to_vec(),
|
||||
v1_extra: Some(V1Extra {
|
||||
recovery_dtbo_offset: 0x0123456789abcdef,
|
||||
recovery_dtbo: b"recovery_dtbo data".to_vec(),
|
||||
}),
|
||||
v2_extra: None,
|
||||
});
|
||||
let sha512 = [
|
||||
0x37, 0x8e, 0xf1, 0xf0, 0xb8, 0x44, 0x0f, 0x9e, 0x16, 0xc0, 0x15, 0x98, 0xa2, 0xb5, 0x06,
|
||||
0x63, 0x59, 0xf4, 0x91, 0xb6, 0x28, 0x03, 0xe6, 0xdc, 0xd2, 0x0d, 0xd7, 0x49, 0x33, 0x63,
|
||||
0x91, 0xd4, 0xa8, 0x24, 0xff, 0xb0, 0x5f, 0x99, 0x2a, 0x9a, 0xb3, 0x66, 0x81, 0x41, 0x69,
|
||||
0xb0, 0xbc, 0xe2, 0x5b, 0x33, 0x3f, 0x39, 0x6a, 0xa8, 0xbd, 0xe1, 0x15, 0x3e, 0x51, 0x5a,
|
||||
0x2a, 0x9d, 0x23, 0x90,
|
||||
];
|
||||
|
||||
round_trip(&image, &sha512, 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn round_trip_v2() {
|
||||
let data = include_bytes!(concat!(
|
||||
env!("CARGO_MANIFEST_DIR"),
|
||||
"/tests/data/boot_v2.img",
|
||||
));
|
||||
round_trip(data, 2);
|
||||
let image = BootImage::V0Through2(BootImageV0Through2 {
|
||||
kernel_addr: 0x01234567,
|
||||
ramdisk_addr: 0x89abcdef,
|
||||
second_addr: 0x02468ace,
|
||||
tags_addr: 0x13579bdf,
|
||||
page_size: 4096,
|
||||
os_version: 0x76543210,
|
||||
name: repeat("Name", 16),
|
||||
cmdline: repeat("Cmdline", 512),
|
||||
id: [
|
||||
0x00112233, 0x44556677, 0x8899aabb, 0xccddeeff, 0xffeeddcc, 0xbbaa9988, 0x77665544,
|
||||
0x33221100,
|
||||
],
|
||||
extra_cmdline: repeat("ExtraCmdline", 1024),
|
||||
kernel: b"kernel data".to_vec(),
|
||||
ramdisk: b"ramdisk data".to_vec(),
|
||||
second: b"second data".to_vec(),
|
||||
v1_extra: Some(V1Extra {
|
||||
recovery_dtbo_offset: 0x0123456789abcdef,
|
||||
recovery_dtbo: b"recovery_dtbo data".to_vec(),
|
||||
}),
|
||||
v2_extra: Some(V2Extra {
|
||||
dtb_addr: 0xfedcba9876543210,
|
||||
dtb: b"dtb data".to_vec(),
|
||||
}),
|
||||
});
|
||||
let sha512 = [
|
||||
0x04, 0x24, 0x5b, 0xb7, 0x07, 0x82, 0xa9, 0x08, 0x68, 0xb9, 0xc9, 0x65, 0x1f, 0x53, 0xd7,
|
||||
0x6c, 0xcf, 0xf3, 0x48, 0x58, 0x9a, 0xd4, 0xb1, 0xf3, 0xd8, 0x6f, 0x95, 0x10, 0x70, 0x2f,
|
||||
0x53, 0x30, 0x60, 0x60, 0xe4, 0x68, 0xd9, 0x84, 0xe8, 0x0a, 0xf3, 0x12, 0xb3, 0xa3, 0x1b,
|
||||
0x06, 0x88, 0x2f, 0x5d, 0x34, 0x5a, 0xea, 0x8f, 0xbb, 0x54, 0x49, 0x3c, 0xc1, 0x9c, 0xc6,
|
||||
0x24, 0x80, 0x03, 0xde,
|
||||
];
|
||||
|
||||
round_trip(&image, &sha512, 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn round_trip_v3() {
|
||||
let data = include_bytes!(concat!(
|
||||
env!("CARGO_MANIFEST_DIR"),
|
||||
"/tests/data/boot_v3.img",
|
||||
));
|
||||
round_trip(data, 3);
|
||||
let image = BootImage::V3Through4(BootImageV3Through4 {
|
||||
os_version: 0x01234567,
|
||||
reserved: [0x00112233, 0x44556677, 0x8899aabb, 0xccddeeff],
|
||||
cmdline: repeat("Cmdline", 1536),
|
||||
v4_extra: None,
|
||||
kernel: b"kernel data".to_vec(),
|
||||
ramdisk: b"ramdisk data".to_vec(),
|
||||
});
|
||||
let sha512 = [
|
||||
0x30, 0xea, 0x77, 0x0a, 0xd3, 0x24, 0x6a, 0x3f, 0xf8, 0xdf, 0xe6, 0xd9, 0x5a, 0xa1, 0xd3,
|
||||
0xa4, 0x3b, 0x8a, 0x13, 0x39, 0x5e, 0x58, 0x24, 0x3e, 0x71, 0x31, 0x78, 0xa1, 0x2c, 0xad,
|
||||
0x1d, 0xca, 0x24, 0x12, 0xf5, 0xfb, 0x2c, 0x48, 0xa5, 0x3d, 0xc0, 0x38, 0x55, 0xb6, 0xfd,
|
||||
0xd3, 0x30, 0xe0, 0x69, 0x11, 0x28, 0xd7, 0x29, 0xda, 0x2e, 0x5a, 0x49, 0x5c, 0x39, 0x1d,
|
||||
0xb9, 0xdb, 0x53, 0xe1,
|
||||
];
|
||||
|
||||
round_trip(&image, &sha512, 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn round_trip_v4() {
|
||||
let data = include_bytes!(concat!(
|
||||
env!("CARGO_MANIFEST_DIR"),
|
||||
"/tests/data/boot_v4.img",
|
||||
));
|
||||
round_trip(data, 4);
|
||||
let image = BootImage::V3Through4(BootImageV3Through4 {
|
||||
os_version: 0x01234567,
|
||||
reserved: [0x00112233, 0x44556677, 0x8899aabb, 0xccddeeff],
|
||||
cmdline: repeat("Cmdline", 1536),
|
||||
v4_extra: Some(V4Extra { signature: None }),
|
||||
kernel: b"kernel data".to_vec(),
|
||||
ramdisk: b"ramdisk data".to_vec(),
|
||||
});
|
||||
let sha512 = [
|
||||
0xa8, 0x1d, 0x2b, 0x78, 0x22, 0x45, 0x0b, 0xe7, 0xc2, 0x3a, 0xd8, 0xda, 0x95, 0x49, 0x77,
|
||||
0x18, 0xd0, 0x7b, 0x9b, 0x7f, 0xc7, 0xf6, 0x48, 0xb4, 0x2d, 0x85, 0x6d, 0xe3, 0x5a, 0xa3,
|
||||
0x24, 0xb6, 0x94, 0x56, 0xb9, 0x07, 0x84, 0xdb, 0x50, 0x01, 0xca, 0x6c, 0x86, 0x26, 0x32,
|
||||
0x79, 0x0c, 0xc5, 0x70, 0xcf, 0xcc, 0x7f, 0xc3, 0x5b, 0x96, 0x56, 0x23, 0x5c, 0xd0, 0x50,
|
||||
0xb0, 0x98, 0xdb, 0x4a,
|
||||
];
|
||||
|
||||
round_trip(&image, &sha512, 4);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn round_trip_v4_vts() {
|
||||
let data = include_bytes!(concat!(
|
||||
env!("CARGO_MANIFEST_DIR"),
|
||||
"/tests/data/boot_v4_vts.img",
|
||||
));
|
||||
round_trip(data, 4);
|
||||
let mut header = Header {
|
||||
required_libavb_version_major: 1,
|
||||
required_libavb_version_minor: 0,
|
||||
algorithm_type: AlgorithmType::Sha256Rsa4096,
|
||||
hash: vec![], // autogenerated
|
||||
signature: vec![], // autogenerated
|
||||
public_key: vec![], // autogenerated
|
||||
public_key_metadata: vec![],
|
||||
descriptors: vec![Descriptor::Hash(HashDescriptor {
|
||||
image_size: 12288,
|
||||
hash_algorithm: "sha256".to_owned(),
|
||||
partition_name: "boot".to_owned(),
|
||||
salt: vec![0x64, 0x30, 0x30, 0x64, 0x66, 0x30, 0x30, 0x64],
|
||||
root_digest: vec![
|
||||
0xab, 0xd5, 0x48, 0x3e, 0x11, 0xe7, 0x94, 0x0c, 0xb9, 0xbf, 0x38, 0x75, 0x87, 0xa4,
|
||||
0xa1, 0x65, 0x99, 0x81, 0xa1, 0xb8, 0x39, 0x62, 0xb7, 0xc1, 0xfa, 0xf1, 0xb0, 0xcd,
|
||||
0x63, 0x07, 0xd4, 0x49,
|
||||
],
|
||||
flags: 0,
|
||||
reserved: [0; 60],
|
||||
})],
|
||||
rollback_index: 0,
|
||||
flags: 0,
|
||||
rollback_index_location: 0,
|
||||
release_string: "avbtool 1.2.0".to_owned(),
|
||||
reserved: [0; 80],
|
||||
};
|
||||
|
||||
let key = get_test_key();
|
||||
header.sign(&key).unwrap();
|
||||
assert_eq!(header.verify().unwrap().unwrap(), key.to_public_key());
|
||||
|
||||
let image = BootImage::V3Through4(BootImageV3Through4 {
|
||||
os_version: 0x01234567,
|
||||
reserved: [0x00112233, 0x44556677, 0x8899aabb, 0xccddeeff],
|
||||
cmdline: repeat("Cmdline", 1536),
|
||||
v4_extra: Some(V4Extra {
|
||||
signature: Some(header),
|
||||
}),
|
||||
kernel: b"kernel data".to_vec(),
|
||||
ramdisk: b"ramdisk data".to_vec(),
|
||||
});
|
||||
let sha512 = [
|
||||
0x19, 0x47, 0x15, 0x3c, 0x1f, 0x62, 0x84, 0xee, 0xbc, 0x16, 0x9e, 0x5a, 0xf2, 0x45, 0x2a,
|
||||
0xf7, 0x40, 0xc2, 0x18, 0x7f, 0x23, 0xb2, 0xa4, 0x20, 0x10, 0xdf, 0xb1, 0x5c, 0xf2, 0x7f,
|
||||
0x6f, 0x79, 0x22, 0x1d, 0x29, 0x27, 0x78, 0xea, 0xb3, 0x9e, 0x1f, 0xfe, 0xeb, 0xc8, 0x9f,
|
||||
0xe6, 0xef, 0xce, 0xa2, 0x28, 0x0b, 0x05, 0x1d, 0x52, 0xff, 0xab, 0xd4, 0x6f, 0x87, 0x11,
|
||||
0xd9, 0xb6, 0x5f, 0x2e,
|
||||
];
|
||||
|
||||
round_trip(&image, &sha512, 4);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn round_trip_vendor_v3() {
|
||||
let data = include_bytes!(concat!(
|
||||
env!("CARGO_MANIFEST_DIR"),
|
||||
"/tests/data/vendor_v3.img",
|
||||
));
|
||||
round_trip(data, 3);
|
||||
let image = BootImage::VendorV3Through4(VendorBootImageV3Through4 {
|
||||
page_size: 4096,
|
||||
kernel_addr: 0x01234567,
|
||||
ramdisk_addr: 0x89abcdef,
|
||||
cmdline: repeat("Cmdline", 2048),
|
||||
tags_addr: 0xfedcba98,
|
||||
name: repeat("Name", 16),
|
||||
dtb: b"dtb data".to_vec(),
|
||||
dtb_addr: 0x76543210,
|
||||
ramdisks: vec![b"ramdisk data".to_vec()],
|
||||
v4_extra: None,
|
||||
});
|
||||
let sha512 = [
|
||||
0x17, 0x18, 0xb9, 0x67, 0x4c, 0x82, 0x71, 0x98, 0x6a, 0x8a, 0xb8, 0x85, 0x3c, 0x77, 0x9e,
|
||||
0x27, 0xeb, 0xce, 0x2a, 0x23, 0x04, 0x63, 0x7c, 0x94, 0xd4, 0xad, 0x1f, 0x3c, 0xee, 0x7e,
|
||||
0x41, 0x8b, 0xa8, 0xd9, 0x35, 0xec, 0xf2, 0xc1, 0x52, 0x3a, 0xd9, 0x5b, 0xbe, 0x63, 0xe8,
|
||||
0x00, 0xd2, 0x23, 0x4e, 0x37, 0x76, 0x31, 0x5a, 0xfc, 0x63, 0x43, 0x32, 0x34, 0x30, 0xf6,
|
||||
0x3e, 0x2e, 0x3e, 0x66,
|
||||
];
|
||||
|
||||
round_trip(&image, &sha512, 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn round_trip_vendor_v4() {
|
||||
let data = include_bytes!(concat!(
|
||||
env!("CARGO_MANIFEST_DIR"),
|
||||
"/tests/data/vendor_v4.img",
|
||||
));
|
||||
round_trip(data, 4);
|
||||
let board_id = [
|
||||
0x00112233, 0x44556677, 0x8899aabb, 0xccddeeff, 0xffeeddcc, 0xbbaa9988, 0x77665544,
|
||||
0x33221100, 0x004488cc, 0x115599dd, 0x2266aaee, 0x3377bbff, 0xffbb7733, 0xeeaa6622,
|
||||
0xdd995511, 0xcc884400,
|
||||
];
|
||||
let image = BootImage::VendorV3Through4(VendorBootImageV3Through4 {
|
||||
page_size: 2048,
|
||||
kernel_addr: 0x01234567,
|
||||
ramdisk_addr: 0x89abcdef,
|
||||
cmdline: repeat("Cmdline", 2048),
|
||||
tags_addr: 0xfedcba98,
|
||||
name: repeat("Name", 16),
|
||||
dtb: b"dtb data".to_vec(),
|
||||
dtb_addr: 0x76543210,
|
||||
ramdisks: vec![
|
||||
b"ramdisk 0 data".to_vec(),
|
||||
b"ramdisk 1 data".to_vec(),
|
||||
b"ramdisk 2 data".to_vec(),
|
||||
b"ramdisk 3 data".to_vec(),
|
||||
],
|
||||
v4_extra: Some(VendorV4Extra {
|
||||
ramdisk_metas: vec![
|
||||
RamdiskMeta {
|
||||
ramdisk_type: bootimage::VENDOR_RAMDISK_TYPE_NONE,
|
||||
ramdisk_name: repeat("None", 32),
|
||||
board_id,
|
||||
},
|
||||
RamdiskMeta {
|
||||
ramdisk_type: bootimage::VENDOR_RAMDISK_TYPE_PLATFORM,
|
||||
ramdisk_name: repeat("Platform", 32),
|
||||
board_id,
|
||||
},
|
||||
RamdiskMeta {
|
||||
ramdisk_type: bootimage::VENDOR_RAMDISK_TYPE_RECOVERY,
|
||||
ramdisk_name: repeat("Recovery", 32),
|
||||
board_id,
|
||||
},
|
||||
RamdiskMeta {
|
||||
ramdisk_type: bootimage::VENDOR_RAMDISK_TYPE_DLKM,
|
||||
ramdisk_name: repeat("Dlkm", 32),
|
||||
board_id,
|
||||
},
|
||||
],
|
||||
bootconfig: "bootconfig data".to_owned(),
|
||||
}),
|
||||
});
|
||||
let sha512 = [
|
||||
0x0e, 0x3f, 0x86, 0x9e, 0xad, 0x98, 0xbb, 0x53, 0xc7, 0xc4, 0x3f, 0xb8, 0xc6, 0x06, 0xdc,
|
||||
0xb2, 0xe5, 0x47, 0x66, 0xe3, 0xaf, 0x2c, 0xa4, 0x91, 0x8d, 0x4b, 0xc5, 0x70, 0x1e, 0x51,
|
||||
0x19, 0x23, 0x7c, 0xab, 0x40, 0x24, 0x95, 0xef, 0xc8, 0x65, 0xdb, 0x5f, 0x0a, 0x41, 0x93,
|
||||
0xff, 0x6c, 0x22, 0xb4, 0x9a, 0xe2, 0x20, 0xc1, 0x95, 0xa0, 0x3c, 0xc2, 0x13, 0xdb, 0xc8,
|
||||
0x24, 0x33, 0x77, 0x75,
|
||||
];
|
||||
|
||||
round_trip(&image, &sha512, 4);
|
||||
}
|
||||
|
||||
+50
-8
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2023 Andrew Gunnerson
|
||||
* SPDX-FileCopyrightText: 2023-2024 Andrew Gunnerson
|
||||
* SPDX-License-Identifier: GPL-3.0-only
|
||||
*/
|
||||
|
||||
@@ -7,22 +7,64 @@ use std::io::{self, Cursor};
|
||||
|
||||
use avbroot::{
|
||||
self,
|
||||
format::cpio::{CpioEntryType, CpioReader, CpioWriter},
|
||||
format::cpio::{CpioEntry, CpioEntryData, CpioEntryType, CpioReader, CpioWriter},
|
||||
util,
|
||||
};
|
||||
|
||||
fn generate_archive() -> Vec<u8> {
|
||||
let writer = Cursor::new(Vec::new());
|
||||
let mut cpio_writer = CpioWriter::new(writer, false);
|
||||
|
||||
for entry in [
|
||||
CpioEntry::new_symlink(b"symlink", b"target"),
|
||||
CpioEntry::new_directory(b"directory", 0o755),
|
||||
CpioEntry::new_file(b"file", 0o644, CpioEntryData::Data(b"foobar".to_vec())),
|
||||
CpioEntry {
|
||||
path: b"reserved".to_vec(),
|
||||
data: CpioEntryData::Size(0),
|
||||
inode: 12345,
|
||||
file_type: CpioEntryType::Reserved,
|
||||
file_mode: 0o4777,
|
||||
uid: 12345678,
|
||||
gid: 87654321,
|
||||
nlink: 2,
|
||||
mtime: 1700000000,
|
||||
dev_maj: 2222,
|
||||
dev_min: 3333,
|
||||
rdev_maj: 4444,
|
||||
rdev_min: 5555,
|
||||
crc32: 0xfedcba09,
|
||||
},
|
||||
] {
|
||||
cpio_writer.start_entry(&entry).unwrap();
|
||||
}
|
||||
|
||||
let writer = cpio_writer.finish().unwrap();
|
||||
let data = writer.into_inner();
|
||||
|
||||
assert_eq!(
|
||||
ring::digest::digest(&ring::digest::SHA512, &data).as_ref(),
|
||||
[
|
||||
0xb0, 0x51, 0xac, 0x28, 0x6f, 0x78, 0xe2, 0xe7, 0x45, 0xa0, 0x52, 0x7c, 0xff, 0x42,
|
||||
0x30, 0x55, 0xbd, 0x64, 0x7d, 0x4e, 0xb8, 0xe6, 0x95, 0xe5, 0x9b, 0xd1, 0x13, 0xd6,
|
||||
0x43, 0x0e, 0x32, 0xb2, 0x4e, 0x62, 0xa4, 0x55, 0x64, 0x48, 0xb7, 0x32, 0x26, 0x57,
|
||||
0x75, 0x07, 0xf5, 0xa6, 0x0f, 0x18, 0xc3, 0x9e, 0x9f, 0x06, 0xdb, 0xa4, 0xf7, 0xeb,
|
||||
0x5e, 0x8f, 0xce, 0xd0, 0x2b, 0x54, 0x39, 0x57
|
||||
],
|
||||
);
|
||||
|
||||
data
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn round_trip_archive() {
|
||||
let data = include_bytes!(concat!(
|
||||
env!("CARGO_MANIFEST_DIR"),
|
||||
"/tests/data/archive.cpio",
|
||||
));
|
||||
let data = generate_archive();
|
||||
assert_ne!(data.len() % 512, 0);
|
||||
|
||||
for pad_to_block_size in [false, true] {
|
||||
println!("Pad to block size: {pad_to_block_size}");
|
||||
|
||||
let reader = Cursor::new(data);
|
||||
let reader = Cursor::new(&data);
|
||||
let mut cpio_reader = CpioReader::new(reader, false);
|
||||
|
||||
let writer = Cursor::new(Vec::new());
|
||||
@@ -40,7 +82,7 @@ fn round_trip_archive() {
|
||||
let new_data = writer.get_ref().as_slice();
|
||||
|
||||
if pad_to_block_size {
|
||||
assert!(new_data.starts_with(data));
|
||||
assert!(new_data.starts_with(&data));
|
||||
assert!(util::is_zero(&new_data[data.len()..]));
|
||||
assert_eq!(new_data.len() % 512, 0);
|
||||
} else {
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
Binary file not shown.
Binary file not shown.
Reference in New Issue
Block a user