Compare commits

..

190 Commits

Author SHA1 Message Date
Andrew Gunnerson 83ab475c11 Version 3.5.0
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-08-15 20:45:04 -04:00
Andrew Gunnerson fe54640029 CHANGELOG.md: Add entry for PR #331
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-08-15 20:44:32 -04:00
Andrew Gunnerson fec1840a5f Add subcommands for packing and unpacking payload binaries
Some devices have "full" OTAs where the payload is missing the recovery
partition. These subcommands make it possible to manually add back the
missing image. Given the strict requirements for how the OTA zip is laid
out and signed, users can't just replace payload.bin in a zip and call
it a day, but it's sufficient for feeding a modified input to
`avbroot ota patch`.

Issue: #328

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-08-15 20:38:52 -04:00
Andrew Gunnerson 395f6934ff CHANGELOG.md: Add entry for PR #329
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-08-14 22:07:47 -04:00
Andrew Gunnerson a83b2fbfa9 Update dependencies
prost and protox have breaking changes, but none that affect avbroot.

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-08-14 21:06:21 -04:00
Andrew Gunnerson e18ef20e4d Version 3.4.1
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-07-28 18:04:20 -04:00
Andrew Gunnerson b140620ed3 CHANGELOG.md: Add entry for PR #323
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-07-28 18:03:16 -04:00
Andrew Gunnerson dd9d8959fd Add support for Magisk 27006
Version 27006 now requires init-ld to be included in the ramdisk.

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-07-28 17:58:13 -04:00
Andrew Gunnerson 9a7cece973 CHANGELOG.md: Add entry for PR #321
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-07-15 21:44:29 -04:00
Andrew Gunnerson aac3aded78 Update all dependencies
Version 1.6.0 of the bytes library was yanked. There's no vulnerability
that impacts avbroot, but it was tripping the cargo-deny checks.

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-07-15 21:32:14 -04:00
Andrew Gunnerson 24320d4fae README.md: Move signature verification instructions to shared repo
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-07-14 18:23:55 -04:00
Ivan Katrovsky 6800ae073e README.ru.md: update translation
Sync with https://github.com/chenxiaolong/avbroot/commit/4d90ee2ac6223ec9e4ca1b1b77d84973ad90cd75

Signed-off-by: Ivan Katrovsky <notbugreporter@proton.me>
2024-07-06 02:37:41 -04:00
Andrew Gunnerson 4d90ee2ac6 README.md: Document that uninstalling Magisk will also flash an unsigned boot image
Closes: #318

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-07-05 19:45:27 -04:00
Ivan Katrovsky 6b087c0844 README.ru.md: update translation
Sync with https://github.com/chenxiaolong/avbroot/commit/d384a8a99bda591a6299f8f76f693eeb1725cb0f

Signed-off-by: Ivan Katrovsky <notbugreporter@proton.me>
2024-07-05 15:46:26 +03:00
Andrew Gunnerson d384a8a99b README.md: Document that fastboot >=34 is required
Older versions of fastboot have bugs that cause the reboot to fastbootd
mode to be skipped, causing failures when flashing dynamic partitions.

Fixes: #314

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-07-04 16:20:22 -04:00
Andrew Gunnerson bc358c62af Version 3.4.0
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-06-26 19:52:44 -04:00
Andrew Gunnerson fc05cb901a CHANGELOG.md: Add entry for PR #312
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-06-26 19:52:11 -04:00
Andrew Gunnerson 031ac8aa31 cli/avb: Add --public-key option for extract-avb subcommand
Issue: #312

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-06-26 19:46:24 -04:00
Andrew Gunnerson 4a1dab4069 Add support for signing with an external program
By default, the helper program is invoked in a way that is compatible
with avbtool's --signing_helper. However, the arguments have been
extended slightly to allow passing in the passphrase file or environment
variable for non-interactive use.

Fixes: #310

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-06-26 19:46:22 -04:00
Andrew Gunnerson bf885e40cf CHANGELOG.md: Add entry for PR #311
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-06-24 18:15:49 -04:00
Andrew Gunnerson c2a441cf78 avb: AlgorithmType: Fail on unknown key types
This is never reachable due to additional checks in every code path that
invokes sign() and verify(), but still better to be correct.

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-06-24 17:54:50 -04:00
Andrew Gunnerson f6c6a9509a Version 3.3.0
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-06-23 15:51:57 -04:00
Andrew Gunnerson 5a00995366 CHANGELOG.md: Add entry for PR #309
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-06-22 18:47:28 -04:00
Andrew Gunnerson edf537aad6 Add payload subcommand for dumping payload.bin header
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-06-22 18:42:46 -04:00
Andrew Gunnerson f006f20209 CHANGELOG.md: Add entry for PR #307
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-06-22 18:38:44 -04:00
Andrew Gunnerson f36c1ca451 payload: Fudge CoW size estimate by 1%
lz4_flex appears to compress system images better than the original lz4
implementation used in libsnapshot_cow, so the estimates are too low.

Fixes: #306

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-06-21 22:36:41 -04:00
Andrew Gunnerson 29b72961a3 Update CoW size estimate when replacing entire dynamic partitions
Otherwise, if the partition size increases or the data becomes more
incompressible, update_engine might fail to flash the partition due to
the CoW block device running out of space.

Since all known VABC-enabled OTAs in the wild currently use CoW v2 with
lz4 compression, this is the only configuration we support. CoW v3 also
exists in AOSP's libsnapshot_cow, but is much more complicated to
implement and is not yet used, even in the Android 15 beta OTAs.

Fixes: #306

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-06-21 21:51:49 -04:00
Andrew Gunnerson e105efb6d3 Version 3.2.3
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-06-14 18:26:14 -04:00
Andrew Gunnerson 2a7df104ed CHANGELOG.md: Add entry for PR #304
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-06-14 18:17:06 -04:00
Andrew Gunnerson 8e52a9cf8c Add support for cross-compiling to Android
The precompiled binaries are compiled for aarch64 API 31, which should
work for every device that avbroot supports.

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-06-14 17:49:15 -04:00
Andrew Gunnerson f9343aa542 Version 3.2.2
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-06-03 19:50:01 -04:00
Andrew Gunnerson 0391d4e2c3 CHANGELOG.md: Add entry for PR #268
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-06-03 18:50:10 -04:00
Andrew Gunnerson c18800dc44 patch/boot: Add entry name/index to zip error messages
`specified file not found in archive` is a useless error message when
there's no additional context.

Issue: #301

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-06-03 18:50:02 -04:00
Andrew Gunnerson 2db90f83c2 Add support for upcoming Magisk Canary 27003
Magisk no longer puts both magisk64 and magisk32 in the ramdisk.
Instead, it just puts a single binary for the target ABI.

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-06-03 18:50:01 -04:00
Andrew Gunnerson 34915e256f Merge pull request #300 from bugreportion/master
README.ru.md: update translation
2024-06-01 10:04:50 -04:00
Ivan Katrovsky 61392eb9d6 README.ru.md: update translation
Signed-off-by: Ivan Katrovsky <notbugreporter@proton.me>
2024-06-01 15:11:29 +03:00
Andrew Gunnerson 029cb4264e README.md: Move supported device list to issue tracker
This makes it a bit easier to keep updated and gather user comments.

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-05-31 18:36:39 -04:00
Andrew Gunnerson 98a7fc811d Merge pull request #298 from bugreportion/master
README.ru.md: update translation
2024-05-31 18:25:32 -04:00
Ivan Katrovsky f4f4ec8e0d README.ru.md: update translation
Signed-off-by: Ivan Katrovsky <notbugreporter@proton.me>
2024-06-01 01:12:47 +03:00
Andrew Gunnerson 99b316f55e CHANGELOG.md: Add entry for PR #297
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-05-30 00:08:58 -04:00
Andrew Gunnerson 50ee90b61a cli/avb: Allow writing new TOML file when packing an image
Previously, there was no way to easily see the values of the recomputed
fields without unpacking the newly built image. This commit adds a new
`--output-info` option to the `avb pack` subcommand to write/overwrite a
new `avb.toml` file.

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-05-30 00:04:05 -04:00
Andrew Gunnerson 2e8bd9f9d4 CHANGELOG.md: Add entry for PR #296
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-05-29 23:52:42 -04:00
Andrew Gunnerson 3fa96714c4 avb: Add support for building minimally sized image
Previously, for resizable images, the user had to guess an appropriate
size for the final image that could fit all the AVB metadata. This
commit adds a new `--recompute-size` option to the `avb pack` subcommand
to generate a minimally sized image.

Fixes: #294

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-05-29 23:42:43 -04:00
Andrew Gunnerson 7a2530a199 README.md: Strongly discourage using any device besides Google Pixels
OnePlus seems to be getting worse and worse with some devices not being
recoverable.

Issue: #290

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-05-29 21:20:24 -04:00
Andrew Gunnerson f84df86ef5 Version 3.2.1
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-05-24 18:27:38 -04:00
Andrew Gunnerson 0c064981dd CHANGELOG.md: Add entry for PR #293
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-05-24 18:26:43 -04:00
Andrew Gunnerson 5645183ecc Merge pull request #293 from chenxiaolong/bump-fec-limits
avb: Bump hashtree and FEC size limits to accommodate 8 GiB images
2024-05-24 18:25:36 -04:00
Andrew Gunnerson bc7358a8d9 avb: Bump hashtree and FEC size limits to accommodate 8 GiB images
Fixes: #291

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-05-24 18:20:28 -04:00
Andrew Gunnerson d47c14ab12 Switch to Rust 1.73.0's builtin div_ceil function
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-05-24 17:57:45 -04:00
Andrew Gunnerson f479fe1a08 Version 3.2.0
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-05-18 21:19:04 -04:00
Andrew Gunnerson df7b76bc59 CHANGELOG.md: Add entry for PR #289
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-05-18 21:13:27 -04:00
Andrew Gunnerson e342b93902 cli/ota: Build list of boot patchers directly in patch_subcommand()
This way, all of the patchers are constructed in the same place and we
don't have to pass their parameters through multiple functions.

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-05-18 21:11:11 -04:00
Andrew Gunnerson d7439e15ae Add support for adding AVB public key to DSU trusted keys
This allows the user to boot GSIs signed by the same key. The option is
disabled by default because some Android builds disable DSU support by
removing all keys to reduce the attack surface. We don't want to
reenable DSU support on these builds unless the user asks for it.

Closes: #286

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-05-18 21:07:16 -04:00
Andrew Gunnerson 0f16f30dfb CHANGELOG.md: Add entry for PR #288
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-05-18 17:47:26 -04:00
Andrew Gunnerson 178c025eca Fix clippy 1.78.0 lints
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-05-18 17:44:01 -04:00
Andrew Gunnerson d6ac94c430 Update all dependencies
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-05-18 17:25:26 -04:00
Andrew Gunnerson 2de260a66c CHANGELOG.md: Add entry for PR #287
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-05-18 17:06:49 -04:00
Andrew Gunnerson e9ba770a15 Switch to my fork of the bzip2-rs library
This includes a fix for yet another infinite-loop-on-drop bug in the
bzip2::write::BzDecoder implementation. This could be easily triggered
when interrupting an avbroot command while it is extracting a bzip2
compressed payload.bin chunk.

Fixes: #285

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-05-18 16:46:58 -04:00
Andrew Gunnerson 3940b31acb Version 3.1.3
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-05-02 19:19:43 -04:00
Andrew Gunnerson e87ee7c4ad CHANGELOG.md: Add entry for PR #279
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-05-02 19:19:21 -04:00
Andrew Gunnerson 0f8e30da37 Merge pull request #279 from chenxiaolong/mac-universal
ci.yml: Build universal binary for macOS
2024-05-02 19:17:31 -04:00
Andrew Gunnerson ae0863e319 ci.yml: Build universal binary for macOS
Fixes: #278

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-05-02 19:10:16 -04:00
Andrew Gunnerson 46662796a0 Version 3.1.2
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-04-27 17:47:09 -04:00
Andrew Gunnerson 237d4e9b11 CHANGELOG.md: Add entry for PR #277
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-04-27 17:46:37 -04:00
Andrew Gunnerson 5adff50b01 cli/ota: Fix incorrectly quoted output and clippy warning
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-04-27 14:31:53 -04:00
Andrew Gunnerson 905eb9245c CHANGELOG.md: Add entry for PR #276
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-04-27 14:30:49 -04:00
Andrew Gunnerson e22f9f5676 tests: Remove all binary test data files
In light of the recently discovered backdoor in the xz project where a
part of the malicious code was distributed in the test files, let's
remove all of our test files and generate them at runtime. While our
test files are very simple and consist mostly of zeros, someone who is
not very familiar with these binary formats would have a harder time
examining them and making sure they aren't malicious. With this change,
the data structures are now plainly visible in the test code.

The files generated at runtime are byte-for-byte identical to the test
files being removed. One can verify by comparing the sha512 checksums of
the files with the sha512 checksums hardcoded in the new test code.

Closes: #265

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-04-27 14:15:08 -04:00
Andrew Gunnerson 17162df1e7 Merge #273: README: Add Russian localization 2024-04-23 21:52:56 -04:00
Ivan Katrovsky b4ea4dfc9c Fix typo 2024-04-24 02:57:08 +03:00
Ivan Katrovsky 5572493acf Added Russian localization
The main page of the repository is now available in Russian.
2024-04-23 18:24:08 +03:00
Andrew Gunnerson 9d38748221 CHANGELOG.md: Add entry for PR #253
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-04-14 22:07:18 -04:00
Andrew Gunnerson fd0408dffe Use fastboot flashall to flash partitions initially
`fastboot flashall` is identical to the `fastboot update` command used
by the Pixel factory images, except it reads from a directory instead of
a zip file. It knows how to flip between the fastboot and fastbootd
modes without user intervention.

Fixes: #252

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-04-14 21:59:30 -04:00
Andrew Gunnerson af0fb1d9b4 Update Github Actions dependencies
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-04-14 18:21:54 -04:00
Andrew Gunnerson 8c965552e8 Merge pull request #269 from schnatterer/feature/extend-docs
README: Add minor clarifications
2024-04-07 17:36:11 -04:00
Johannes Schnatterer 2532476583 README: Add minor clarifications 2024-04-07 21:32:42 +02:00
Andrew Gunnerson cf77999d95 README.md: Fix grammar
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-03-12 18:12:02 -04:00
Andrew Gunnerson 0ade50f49e README.md: Remove mention of my_engineering image
Patching the image was never implemented because it's specific to
OnePlus devices where the bootloader does not respect the custom root of
trust.

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-03-12 17:54:39 -04:00
Andrew Gunnerson 0bad6f8c6f Version 3.1.1
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-03-05 21:59:41 -05:00
Andrew Gunnerson 4ce4863237 CHANGELOG.md: Add entry for PR #261
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-03-05 21:59:07 -05:00
Andrew Gunnerson f615bf48cc crypto: reformat_pem: Strip out irrelevant lines
x509_cert is unable to parse files that contains non-empty lines outside
of the BEGIN CERTIFICATE and END CERTIFICATE markers.

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-03-05 21:42:02 -05:00
Andrew Gunnerson b505b14574 CHANGELOG.md: Add entry for PR #257
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-02-03 20:38:39 -05:00
Andrew Gunnerson e2adff6eed hashtree: Precompute salted SHA-256 context
With the dm-verity hash tree format, each digest is salted, meaning it
first hashes the salt byte string and then the actual data. Previously,
the salt was being rehashed for each digest operation. Instead, the
salted SHA-256 context can be computed once and then cloned when needed.

The performance improvement is pretty minor, but it's worth doing anyway
since it's not any more complex.

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-02-03 20:20:15 -05:00
Andrew Gunnerson 10370692d0 Version 3.1.0
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-02-03 18:51:51 -05:00
Andrew Gunnerson 73b6e9b177 CHANGELOG.md: Add entry for PR #256
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-02-03 18:51:15 -05:00
Andrew Gunnerson 8d25e28ea3 Fix clippy warnings and formatting
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-02-03 18:36:20 -05:00
Andrew Gunnerson 82fc1336f7 Update all dependencies
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-02-03 18:32:59 -05:00
Andrew Gunnerson 953c0c1c5b CHANGELOG.md: Add entry for PR #251
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-02-03 18:23:35 -05:00
Andrew Gunnerson c3b073cece Add tracing support
Instead of println'ing everything, this commit switches the code base to
using the tracing library. There are now proper log levels and multiple
logging output formats. A bunch of new debug and trace-level messages
have also been added to help with future troubleshooting.

By default, the output is kept nice and short. Spans won't be shown
unless the log level is set to debug or lower.

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-02-03 18:16:57 -05:00
Andrew Gunnerson 565efc5ee5 CHANGELOG.md: Add entry for PR #255
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-02-03 14:58:39 -05:00
Andrew Gunnerson 6ef8e548a3 Add support for Magisk v27.0
Upstream Magisk now xz-compresses files in modifies in the ramdisk. This
commit also implements the same in avbroot's MagiskRootPatcher.

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-02-03 14:35:57 -05:00
Andrew Gunnerson 80549346ea README.md: Temporarily suggest manual procedure for initially flashing system.img
We'll switch to using `fastboot flashall` in the future once that has
been implemented and tested.

Fixes: #252

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-01-31 17:39:13 -05:00
Andrew Gunnerson 275b18b2a5 CHANGELOG.md: Add entry for PR #247
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-12-31 16:29:40 -05:00
Andrew Gunnerson 0a68966594 Switch from xz2 to liblzma
liblzma is a maintained fork of xz2. We can now get rid of our own fork
of xz2.

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-12-31 16:20:30 -05:00
Andrew Gunnerson 9ef5629556 CHANGELOG.md: Add entry for PR #246
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-12-30 19:25:49 -05:00
Andrew Gunnerson 1db09eb896 Merge pull request #246 from chenxiaolong/remove_modules
Remove oemunlockonboot module
2023-12-30 19:24:06 -05:00
Andrew Gunnerson a134d7f889 Remove oemunlockonboot module
The module has been split out into another repo [1] so that it can be
versioned separately. It now also supports Magisk's automatic update
mechanism.

Closes: #235

[1] https://github.com/chenxiaolong/OEMUnlockOnBoot

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-12-30 19:16:05 -05:00
Andrew Gunnerson 833ca931e8 Version 3.0.0
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-12-29 20:57:22 -05:00
Andrew Gunnerson 9b6677d28a CHANGELOG.md: Remove entry for PR #220
It was obsoleted by #226.

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-12-29 20:55:06 -05:00
Andrew Gunnerson 12e59fcf53 CHANGELOG.md: Add entry for PR #245
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-12-29 20:52:30 -05:00
Andrew Gunnerson 4bc7363f70 Merge pull request #245 from chenxiaolong/deps
Update all dependencies
2023-12-29 20:51:53 -05:00
Andrew Gunnerson b5ad9dddf6 Update all dependencies
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-12-29 20:44:12 -05:00
Andrew Gunnerson f0aba89549 CHANGELOG.md: Add entry for PR #243
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-12-29 20:41:22 -05:00
Andrew Gunnerson c9633682ef Merge pull request #243 from chenxiaolong/repair_mode
README.md: Document Repair Mode
2023-12-29 20:39:22 -05:00
Andrew Gunnerson da17935972 CHANGELOG.md: Add entry for PR #241
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-12-29 16:18:39 -05:00
Andrew Gunnerson c2140a85e9 Merge pull request #241 from chenxiaolong/e2e
e2e: Switch to using mock OTAs for testing
2023-12-29 16:17:31 -05:00
Andrew Gunnerson 3a3582ce4c e2e: Switch to using mock OTAs for testing
This commit replaces the previous approach of patching real OTAs with
patching mock OTAs. The motivation for this change is to make it
possible to test the system partition otacerts.zip patching without
needing to download huge files. Adding the system image to the stripped
OTAs would increase the file size by an order of magnitude.

The mock OTAs are generated from a set of profiles defined in e2e.toml.
The four included profiles are meant to mimic the OTAs used for testing
before:

* pixel_v4_gki     ~= cheetah
* pixel_v4_non_gki ~= bluejay
* pixel_v3         ~= bramble
* pixel_v2         ~= sunfish

There is no equivalent profile for ossi because newer OnePlus devices no
longer support custom signing keys properly.

The mock OTAs are perfectly valid, structure and signature-wise. They
just don't include any real partition data where possible. They are
initially signed with a different set of keys to ensure that the changes
made by the patching process are actually visible.

With how small the mock OTAs are, testing every profile only takes about
two seconds. Thus, the Github Actions workflow was adjusted to just run
e2e in the same job as the build.

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-12-29 16:07:57 -05:00
Andrew Gunnerson b029116a5a CHANGELOG.md: Add entry for PR #244
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-12-29 15:48:12 -05:00
Andrew Gunnerson af1b01646d Merge pull request #244 from chenxiaolong/system_patch_performance
Optimize repacking of modified system image into payload
2023-12-29 15:47:09 -05:00
Andrew Gunnerson e85102709f Optimize repacking of modified system image into payload
When the original payload extents are all in order and have no gaps, we
can efficiently copy unmodified chunks of the system image from the
original payload into the new payload. Only the chunks containing the
modified regions (`otacerts.zip`, hash tree, FEC data, and AVB metadata)
need to be recompressed. This massively reduces the CPU usage since
usually only <20 MiB need to be recompressed.

If the conditions for the optimized path aren't satisfied (eg. extents
aren't in order or `--replace` is used), then it falls back to splitting
and compressing the entire system image.

This fixes the performance issues introduced in #240. This is probably
the best that we can do given that we now always patch the system image.

Issue: #225

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-12-29 00:02:35 -05:00
Andrew Gunnerson 124629dc45 README.md: Document Repair Mode
Closes: #216

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-12-28 18:10:26 -05:00
Andrew Gunnerson e89c4bdc9e CHANGELOG.md: Add entry for PR #240
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-12-28 18:00:23 -05:00
Andrew Gunnerson c1095b8321 Merge pull request #240 from chenxiaolong/system_otacerts
Add support for replacing otacerts.zip in the system image
2023-12-28 17:49:02 -05:00
Andrew Gunnerson 17504a7f81 Add support for replacing otacerts.zip in the system image
Previously, overriding otacerts.zip in the system partition required the
user to flash a Magisk/KernelSU module that would bind mount over the
file during boot. While this worked well enough, it's insufficient for
unrooted setups, which has become more important since unrooting is the
only safe way to use the new OEM repair mode feature. With the stock
otacerts.zip, the OEM's default OTA updater app could run and install an
OS upgrade that's not signed by the user's key.

With this commit, the raw otacerts.zip bytes in the system partition are
directly replaced with a new zip that contains the user's certificate.
This method was inspired by @pascallj's comment in #216 suggesting
intentionally corrupting the otacerts.zip data in the filesystem.

Because avbroot does not have filesystem parsers for ext4/f2fs/erofs, we
rely on a heuristic-based search on the raw filesystem image. The file
is always smaller than one block (which is at least 4096 bytes on all
known devices), so the file data is stored contiguously on disk and in
the case of erofs, won't be compressed. None of the three filesystems
are copy-on-write and thus, have no filesystem-level data checksums. For
the dm-verity layer one level up, avbroot already knows how to recompute
the hash tree and FEC data.

To ensure that there are no false positives, any match that the search
finds must correctly parse as a valid zip and every entry within the zip
must have a filename that ends in .x509.pem. This matches what
update_engine expects from a proper otacerts.zip file.

Since the new approach is doing a raw search and replace, the old and
new files must have the same size. When the new zip is smaller, null
bytes are added to the zip archive comment field to pad to the correct
size. When the new zip is larger, avbroot will attempt the following to
try and make the file size smaller:

1. Enable zip deflate compression
2. Strip the X.509 signature from the certificate
3. Clear out the issuer RDN sequence from the certificate
4. Clear out the subject RDN sequence from the certificate

The latter three changes work because Android never performs any PKI
operations with the certificate. There is no CA certificate chain. The
X.509 certificate file is nothing more than a way to transport an RSA
public key.

avbroot requires the user's key to be RSA 4096. If the original zip had
the same key size, then none of these shrinking methods are needed. If
it contained an RSA 2048 key, then the first two modifications are
usually sufficient. The latter two modifications should only be needed
if the user picked a really long subject value when generating the
certificate.

With these new changes, the OTA patching time will approximately double
on a system with an SSD and modern CPU. This is dominated by the time it
takes to XZ-compress the system partition image. The compression is
already parallelized and scales linearly with the number of cores.
There's likely not much more that can be done to further speed this up.

Finally, these new changes are currently excluded from the e2e tests
because including the system partition in the stripped OTAs would
increase the file size by an order of magnitude. This could potentially
be solved in the future by generating our own small OTAs to use for
testing instead of running against real device OTAs.

Fixes: #225

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-12-24 18:23:03 -05:00
Andrew Gunnerson d8190143bf CHANGELOG.md: Add entry for PR #237
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-12-22 16:17:06 -05:00
Andrew Gunnerson 0944abcea2 Merge pull request #237 from chenxiaolong/autodetect
Allow boot image autodetection to inspect boot images
2023-12-22 16:14:52 -05:00
Andrew Gunnerson 249c36d8c3 Allow boot image autodetection to inspect boot images
During patching, all boot images are now extracted and the individual
patchers can inspect them to determine which ones need modifications.
This replaces the previous mechanism of detecting which boot images to
patch based on the name alone.

With this new method, the --boot-partition and --otacerts-partitions
options are no longer needed. The former option is kept (but ignored
with a warning message) for backwards compatibility, but the latter is
completely removed because it never made it to a stable release.

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-12-22 00:35:32 -05:00
Andrew Gunnerson 446e87289f CHANGELOG.md: Add entry for PR #234
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-12-17 22:14:44 -05:00
Andrew Gunnerson 04b8e3c315 Merge pull request #234 from chenxiaolong/fec_param_order
fec: Change update() parameter order to be more consistent
2023-12-17 22:14:09 -05:00
Andrew Gunnerson 03809a1e88 fec: Change update() parameter order to be more consistent
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-12-17 22:07:00 -05:00
Andrew Gunnerson 1fcb29532d CHANGELOG.md: Add entry for PR #233
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-12-17 21:08:28 -05:00
Andrew Gunnerson b383c8a0ab Merge pull request #233 from chenxiaolong/hash_tree
Split dm-verify hash tree logic out of avb module
2023-12-17 21:06:07 -05:00
Andrew Gunnerson 7ea8e8fa9e Split dm-verify hash tree logic out of avb module
* Refactor hash tree computation to work on a preallocated hash tree
  buffer. This makes it possible to partially update a hash tree, which
  is now supported.

* Add new subcommands for working with hash trees. There's no standard
  header format for dm-verity information, so these commands write hash
  tree files with a custom header. The commands are not really useful
  outside of debugging avbroot's hash tree implementation.

  Using AVB was considered, but it has no support for the hash tree data
  living in a separate file from the input. If other parties agree on a
  standard header in the future, avbroot will switch to that format.

* Add tests for the hash tree implementation.

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-12-17 20:48:00 -05:00
Andrew Gunnerson ce43b3c657 CHANGELOG.md: Add entry for PR #232
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-12-17 15:08:43 -05:00
Andrew Gunnerson 05e5e59d65 Merge pull request #232 from chenxiaolong/small_hash_tree
avb: hashtree: Fix incorrect I/O read size for input smaller than one block
2023-12-17 15:07:51 -05:00
Andrew Gunnerson 72b8120bf9 avb: hashtree: Fix incorrect I/O read size for input smaller than one block
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-12-17 15:00:50 -05:00
Andrew Gunnerson 8110f82b17 CHANGELOG.md: Add entry for PR #231
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-12-17 14:59:47 -05:00
Andrew Gunnerson 64dbfa241e Merge pull request #231 from chenxiaolong/overlap
util: merge_overlapping: Skip empty and invalid ranges
2023-12-17 14:59:11 -05:00
Andrew Gunnerson 7041c8ef2c util: merge_overlapping: Skip empty and invalid ranges
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-12-17 14:50:21 -05:00
Andrew Gunnerson 7296eed51f CHANGELOG.md: Add entry for PR #230
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-12-17 00:48:36 -05:00
Andrew Gunnerson 698810d256 Merge pull request #230 from chenxiaolong/fec_update
fec: Add support for partially updating FEC data
2023-12-17 00:47:45 -05:00
Andrew Gunnerson acf57272a7 fec: Add support for partially updating FEC data
FEC data can be updated efficiently with "round" granularity when the
regions where the input file was modified are known.

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-12-17 00:39:04 -05:00
Andrew Gunnerson 2b50e4527f CHANGELOG.md: Add entry for PR #229
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-12-16 23:09:09 -05:00
Andrew Gunnerson cd7b5ab23c Merge pull request #229 from chenxiaolong/partition_hashes
cli/ota: Also verify whole-partition hashes
2023-12-16 23:07:45 -05:00
Andrew Gunnerson f73c451d23 cli/ota: Also verify whole-partition hashes
Previously, when running `avbroot ota verify`, only the payload chunk
hashes were verified.

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-12-16 22:59:33 -05:00
Andrew Gunnerson a801e18595 CHANGELOG.md: Add entry for PR #228
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-12-16 22:51:06 -05:00
Andrew Gunnerson 3932ff9092 Merge pull request #228 from chenxiaolong/parallel_compress
Split new partition images into chunks and compress in parallel
2023-12-16 22:49:22 -05:00
Andrew Gunnerson 1a36fbd46c Split new partition images into chunks and compress in parallel
Previously, all replacement partition images (those that have been
patched or `--replace`d) were compressed as a whole, which would be very
slow for larger images. Instead, we'll split the images into 2 MiB
chunks and compress them in parallel. This more closely matches what
AOSP's payload_generator does and scales linearly with the number of CPU
cores. The compression is less efficient, but the file size generally
only increases by 10s of KiB.

This commit also reworks the implementation so that patched images are
stored in temp files instead of in memory again. In hindsight, doing
everything in memory only made things more complex and causes the memory
usage to blow up when doing things like `--replace system <path>`.

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-12-16 22:38:18 -05:00
Andrew Gunnerson 077a80f4ce CHANGELOG.md: Add entry for PR #226
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-12-12 21:41:30 -05:00
Andrew Gunnerson 1e43930f3c Merge pull request #226 from chenxiaolong/critical
cli/ota: Limit critical partition check to bootloader-verified partitions
2023-12-12 21:39:39 -05:00
Andrew Gunnerson d00e53fb2a cli/ota: Limit critical partition check to bootloader-verified partitions
dm-verity partitions don't necessarily have AVB descriptors inside the
root vbmeta images. The fstab might verify them against a public key on
disk using the `avb_keys` fs_mgr option. We have no way to statically
check for this scenario and it's becoming increasingly common on newer
OnePlus devices. Instead, we'll just limit the checks to partitions that
are verified by the bootloader.

Fixes: #223

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-12-12 21:30:24 -05:00
Andrew Gunnerson 0b5f3b30cf CHANGELOG.md: Add entry for PR #227
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-12-12 21:30:09 -05:00
Andrew Gunnerson 10c2a0969f Merge pull request #227 from chenxiaolong/symlink
workflows/ci: Fix overwriting symlink for target/output
2023-12-12 21:29:25 -05:00
Andrew Gunnerson 83218a747d workflows/ci: Fix overwriting symlink for target/output
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-12-12 21:19:26 -05:00
Andrew Gunnerson 6fb7d568cd CHANGELOG.md: Add entry for PR #224
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-12-12 19:12:11 -05:00
Andrew Gunnerson 6cbf690e17 Merge pull request #224 from chenxiaolong/static
workflows/ci: Build statically linked executables where possible
2023-12-12 19:07:02 -05:00
Andrew Gunnerson e097f98404 workflows/ci: Build statically linked executables where possible
This way, the precompiled executables can run on distros that ship an
older version of glibc or don't use glibc at all.

Fixes: #222

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-12-12 18:34:02 -05:00
Andrew Gunnerson d5605eabc0 workflows/ci: Use Rust triple instead of LLVM triple
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-12-12 18:05:00 -05:00
Andrew Gunnerson d1bcf2da80 CHANGELOG.md: Add entry for PR #221
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-12-11 18:23:49 -05:00
Andrew Gunnerson bbf8e28a28 Merge pull request #221 from chenxiaolong/otacerts
cli/ota: Add new --otacerts-partition option for the patch subcommand
2023-12-11 18:22:03 -05:00
Andrew Gunnerson dc93b17888 cli/ota: Add new --otacerts-partition option
The autodetection logic for `@otacerts` is based on the presence of the
`recovery`, `vendor_boot`, and `boot` partitions (in that order). Some
devices have `vendor_boot`, but put `system/etc/security/otacerts.zip`
inside `boot`.

With the way things are written now, we don't have the ability to
inspect the actual partition images for the autodetection. It is based
on the name only. So, for now, we'll just allow the user to override the
autodetected partition similar to what we already do with the
`--boot-partition` option.

Issue: #218

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-12-10 23:32:14 -05:00
Andrew Gunnerson f83a408dc4 CHANGELOG.md: Add entry for PR #220
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-12-10 16:03:08 -05:00
Andrew Gunnerson 798fddc4d5 Merge pull request #220 from chenxiaolong/odm
cli/ota: Fine tune `is_critical_to_avb()` for odm partition
2023-12-10 16:01:43 -05:00
Andrew Gunnerson dfed06dcf8 cli/ota: Fine tune is_critical_to_avb() for odm partition
AOSP primarily cares about `odm` and `odm_dlkm`, so update the logic to
check for that specifically. This fixes patching some OTAs that include
an unprotected OEM-specific `odm_ext` image.

Issue: #218

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-12-10 15:54:02 -05:00
Andrew Gunnerson 02fd92a640 CHANGELOG.md: Add entry for PR #219
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-12-10 13:47:33 -05:00
Andrew Gunnerson dd4faf72ae Merge pull request #219 from chenxiaolong/decode-avb
key: Add new decode-avb subcommand
2023-12-10 13:43:58 -05:00
Andrew Gunnerson adbe249edb key: Add new decode-avb subcommand
This does the reverse of `avbroot key extract-avb`. It's useful for
reconstructing a PKCS8-encoded RSA public key when working with the
`public_key` fields in `avb.toml`.

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-12-10 13:29:45 -05:00
Andrew Gunnerson b7028b13a2 CHANGELOG.md: Add entry for PR #214
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-12-03 18:44:11 -05:00
Andrew Gunnerson 8122b0eece Merge pull request #214 from chenxiaolong/RUSTSEC-2023-0071
deny.toml: Temporarily ignore RUSTSEC-2023-0071
2023-12-03 18:41:17 -05:00
Andrew Gunnerson 0613de4ee4 deny.toml: Temporarily ignore RUSTSEC-2023-0071
https://rustsec.org/advisories/RUSTSEC-2023-0071

This is a side-channel vulnerability where secrets can be leaked to an
attacker that is able to measure the timing of a large number of RSA
operations. As of 2023-12-03, there is no released version of the rsa
crate that contains a fix.

For avbroot specifically, this vulnerability is not too critical for a
couple reasons:

1. avbroot performs RSA signing only at the end of lengthy processes
   that involve a lot of disk I/O. It's very expensive to run avbroot
   the millions of times needed to capture a sufficient amount of timing
   data.
2. During a single run of avbroot, it will only perform RSA signing a
   handful of times. To get sufficient measurements, the attacker would
   need to rerun avbroot. If they are able to rerun avbroot, then they
   are also able to just read and steal the private key directly.

avbroot has no network capabilities, so this is not inherently remotely
exploitable.

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-12-03 18:02:48 -05:00
Andrew Gunnerson 1a18eb7f85 README.md: Document OnePlus boot issues
Issue: #186
Issue: #195
Issue: #212

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-12-03 14:58:38 -05:00
Andrew Gunnerson 1e1ca9dcbf CHANGELOG.md: Add entry for PR #211
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-11-22 19:51:39 -05:00
Andrew Gunnerson 6924783f48 Merge pull request #211 from chenxiaolong/lints
Fix new lint warnings from rustc and clippy 1.74.0
2023-11-22 19:50:27 -05:00
Andrew Gunnerson 98071daa33 Fix new lint warnings from rustc and clippy 1.74.0
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-11-22 19:30:52 -05:00
Andrew Gunnerson 8e1adae947 CHANGELOG.md: Add entry for PR #210
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-11-22 19:29:00 -05:00
Andrew Gunnerson d6705f4f00 Merge pull request #210 from chenxiaolong/avb_2.0_format_1.3
avb: Add support for AVB 2.0 format 1.3.0
2023-11-22 19:27:24 -05:00
Andrew Gunnerson a648df1695 avb: Add support for AVB 2.0 format 1.3.0
Format version 1.3.0 adds a new 32-bit big endian `flags` field to
chain descriptors, carved out from the `reserved` array. There is only
one possible flag, which indicates that the target partition is not A/B
and so the bootloader should not append the `_a` or `_b` suffix.

There are no known AVB images in the wild where the first four bytes of
the `reserved` field are not zero. Thus, to keep the logic simple, the
AVB parser will unconditionally parse those bytes as if they were the
`flags` field, regardless of the file format version.

AOSP changes:

* https://android.googlesource.com/platform/external/avb/+/a1fe228b86543a21739c51352f5ce72f134fccfa%5E%21/
* https://android.googlesource.com/platform/external/avb/+/d00d02c390e267ef43d93562864dd6e45966c435%5E%21/

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-11-22 18:47:35 -05:00
Andrew Gunnerson a7c872be3e Version 2.3.3
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-11-15 18:59:43 -05:00
Andrew Gunnerson cf1ab6ecca CHANGELOG.md: Add entry for PR #208
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-11-15 18:58:53 -05:00
Andrew Gunnerson 7364e8d725 Merge pull request #208 from chenxiaolong/descriptors
cli/ota: Merge property and kernel cmdline descriptors from child into parent
2023-11-15 18:57:26 -05:00
Andrew Gunnerson 935a86e72c cli/ota: Merge property and kernel cmdline descriptors from child into parent
Some devices use the legacy Android/ChromiumOS-specific `dm=` kernel
command line option to configure dm-verity without userspace helpers.
These options are specified in kernel command line descriptors in the
system partition's vbmeta header, which need to be merged into the
parent vbmeta image for the bootloader to see them.

This commit adds support for merging property descriptors and kernel
command line descriptors. Property descriptors are merged based on an
exact string match of the property key. Kernel command line descriptors
are merged based on the (non-empty) text before the first equal sign.

Issue: #203

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-11-15 18:39:55 -05:00
Andrew Gunnerson a9a6107043 CHANGELOG.md: Add entry for PR #207
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-11-15 17:46:49 -05:00
Andrew Gunnerson 3c1d5a8bb3 Merge pull request #207 from chenxiaolong/xz
compression: Add support for XZ-compressed ramdisks
2023-11-15 17:11:23 -05:00
Andrew Gunnerson 26ec8098e5 compression: Add support for XZ-compressed ramdisks
`lineage-20.0-20231109-nightly-taimen-signed.zip` is an example of an
OTA that uses XZ-compressed ramdisks.

Issue: #203

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-11-15 16:37:00 -05:00
Andrew Gunnerson 2a293147b1 Version 2.3.2
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-11-14 19:19:15 -05:00
Andrew Gunnerson aea12c8d58 CHANGELOG.md: Add entry for PR #206
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-11-14 19:18:43 -05:00
Andrew Gunnerson 5545b0fe1b Merge pull request #206 from chenxiaolong/promote
cli/avb: Promote insecure hash algorithms to secure ones
2023-11-14 19:17:18 -05:00
Andrew Gunnerson 9c818fb165 cli/avb: Promote insecure hash algorithms to secure ones
This is done unconditionally because there shouldn't be any real-world
device that uses AVB2 and has a kernel compiled without sha256 support.

Issue: #203

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-11-14 19:10:07 -05:00
Andrew Gunnerson d174af8969 CHANGELOG.md: Add entry for PR #205
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-11-14 18:54:14 -05:00
Andrew Gunnerson 2cf11094de Merge pull request #205 from chenxiaolong/cmdline
cli/avb: Update kernel cmdline descriptor for devices that use `dm=`
2023-11-14 18:52:16 -05:00
Andrew Gunnerson 6f565969b7 cli/avb: Update kernel cmdline descriptor for devices that use dm=
Older Pixel devices (and ChromiumOS) specify the dm-verity options on
the kernel command line using a custom `dm=` parameter instead of using
dm-init or a userspace helper. This commit updates the avb pack and
repack commands to automatically update the relevant kernel command line
descriptor if it exists.

Issue: #203

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-11-14 18:36:07 -05:00
Andrew Gunnerson da2eb6b717 Merge pull request #204 from chenxiaolong/readme
README.md: Make sections easier to follow
2023-11-13 15:50:01 -05:00
Andrew Gunnerson 2f8d0264eb README.md: Make sections easier to follow
* Split out requirements from the warnings/caveats section and move it
  to the top. Hopefully this helps new users determine whether they can
  even use avbroot quicker.
* Split usage section into separate usage, initial install, and updates
  sections. This should hopefully make the steps much easier to follow
  without a bunch of steps being prefixed with `[Initial setup only]`.
* Explicitly state what arguments are required for Magisk, KernelSU, and
  unrooted setups instead of directing users to the advanced usage
  section.
* Don't assume that everyone will use Magisk in the other sections.
* Explicitly state the assumption that the device should already running
  the OS build that the user wants to patch during initial install.
* Add step to updates section for installing an updated Magisk or
  KernelSU app.
* Move command for building the modules from the modules section to the
  building from source section.
* Update sample error message in the clear vbmeta flags section to match
  what avbroot will actually print out.
* General rewording to try and make things clearer (especially reducing
  parenthesized parts).

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-11-13 03:51:17 -05:00
Andrew Gunnerson ec1fe74900 CHANGELOG.md: Add entry for PR #202
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-11-12 18:53:13 -05:00
Andrew Gunnerson b3b7c7b738 Merge pull request #202 from chenxiaolong/error
cli/ota: Improve error reporting when descriptor types are mismatched
2023-11-12 18:51:56 -05:00
Andrew Gunnerson 2f1ee1ae4f cli/ota: Improve error reporting when descriptor types are mismatched
Issue: #201

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-11-12 18:32:55 -05:00
Andrew Gunnerson cb62996b9f Version 2.3.1
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-11-06 16:59:49 -05:00
Andrew Gunnerson 54c06835c5 CHANGELOG.md: Add entry for PR #199
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-11-06 16:59:16 -05:00
Andrew Gunnerson 65a1b80da5 Merge pull request #199 from chenxiaolong/magisk_26.4
boot.rs: Bump Magisk version upper bound to 26500
2023-11-06 16:58:50 -05:00
Andrew Gunnerson 76ff1ddcda boot.rs: Bump Magisk version upper bound to 26500
There are no upstream changes that impact avbroot.

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-11-06 16:40:14 -05:00
Andrew Gunnerson 0a4dda14cd README.md: Clarify that dmesg step should be run on the device
Fixes: #198

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-10-29 14:02:57 -04:00
80 changed files with 8948 additions and 4322 deletions
@@ -1,48 +0,0 @@
name: Preload img cache
inputs:
cache-key-prefix:
description: 'Device cache-key prefix'
required: true
device:
description: 'Device name'
required: true
runs:
using: "composite"
steps:
- uses: actions/cache@v3
id: cache-img
with:
key: ${{ inputs.cache-key-prefix }}${{ inputs.device }}
# Make sure any changes to path are also reflected in ci.yml setup
path: e2e/files/${{ inputs.device }}-sparse.tar
- if: ${{ steps.cache-img.outputs.cache-hit }}
name: Extracting image from sparse archive
shell: sh
working-directory: e2e/files
run: tar -xf ${{ inputs.device }}-sparse.tar
- name: Restore e2e executable
if: ${{ ! steps.cache-img.outputs.cache-hit }}
uses: actions/cache/restore@v3
with:
key: e2e-${{ github.sha }}-${{ runner.os }}
fail-on-cache-miss: true
path: |
target/release/e2e
target/release/e2e.exe
- name: Downloading device image for ${{ inputs.device }}
if: ${{ ! steps.cache-img.outputs.cache-hit }}
shell: sh
working-directory: e2e
run: ../target/release/e2e download --stripped -d ${{ inputs.device }}
- if: ${{ ! steps.cache-img.outputs.cache-hit }}
name: Creating sparse archive from image
shell: sh
working-directory: e2e/files
run: |
tar --sparse -cf ${{ inputs.device }}-sparse.tar \
${{ inputs.device }}/*.stripped
@@ -1,31 +0,0 @@
name: Preload Magisk
inputs:
cache-key:
description: 'Magisk cache-key'
required: true
runs:
using: "composite"
steps:
- uses: actions/cache@v3
id: cache-magisk
with:
key: ${{ inputs.cache-key }}
# Make sure any changes to path are also reflected in ci.yml setup
path: e2e/files/magisk
- name: Restore e2e executable
if: ${{ ! steps.cache-magisk.outputs.cache-hit }}
uses: actions/cache/restore@v3
with:
key: e2e-${{ github.sha }}-${{ runner.os }}
fail-on-cache-miss: true
path: |
target/release/e2e
target/release/e2e.exe
- name: Downloading Magisk
if: ${{ ! steps.cache-magisk.outputs.cache-hit }}
shell: sh
working-directory: e2e
run: ../target/release/e2e download --magisk
+112 -167
View File
@@ -13,24 +13,60 @@ concurrency:
jobs:
build:
runs-on: ${{ matrix.os }}
runs-on: ${{ matrix.artifact.os }}
env:
CARGO_TERM_COLOR: always
RUSTFLAGS: -C strip=symbols
# https://github.com/rust-lang/rust/issues/78210
RUSTFLAGS: -C strip=symbols -C target-feature=+crt-static
TARGETS: ${{ join(matrix.artifact.targets, ' ') || matrix.artifact.name }}
ANDROID_API: ${{ matrix.artifact.android_api }}
strategy:
fail-fast: false
matrix:
os:
- ubuntu-latest
- windows-latest
- macos-latest
artifact:
- os: ubuntu-latest
name: x86_64-unknown-linux-gnu
- os: windows-latest
name: x86_64-pc-windows-msvc
- os: macos-latest
name: universal-apple-darwin
targets:
- aarch64-apple-darwin
- x86_64-apple-darwin
combine: lipo
# ubuntu-latest is not 24.04 yet and 22.04's qemu-user-static segfaults.
- os: ubuntu-24.04
name: aarch64-linux-android31
targets:
- aarch64-linux-android
android_api: '31'
steps:
- name: Check out repository
uses: actions/checkout@v3
uses: actions/checkout@v4
with:
# For git describe
fetch-depth: 0
- name: Install qemu-user-static
if: ${{ contains(matrix.artifact.name, 'android') }}
shell: bash
run: |
sudo apt-get -y update
sudo apt-get -y install qemu-user-static
- name: Set Android temporary directory
if: ${{ contains(matrix.artifact.name, 'android') }}
shell: bash
run: |
echo "TMPDIR=/tmp" >> "${GITHUB_ENV}"
- name: Install cargo-android
shell: bash
run: |
cargo install \
--git https://github.com/chenxiaolong/cargo-android \
--tag v0.1.1
- name: Get version
id: get_version
shell: bash
@@ -40,179 +76,88 @@ jobs:
| sed -E "s/^v//g;s/([^-]*-g)/r\1/;s/-/./g" \
>> "${GITHUB_OUTPUT}"
- name: Get Rust LLVM target triple
id: get_target
- name: Install toolchains
shell: bash
env:
RUSTC_BOOTSTRAP: '1'
run: |
echo -n 'name=' >> "${GITHUB_OUTPUT}"
rustc -Z unstable-options --print target-spec-json \
| jq -r '."llvm-target"' \
>> "${GITHUB_OUTPUT}"
for target in ${TARGETS}; do
rustup target add "${target}"
done
- name: Cache Rust dependencies
uses: Swatinem/rust-cache@v2
- name: Clippy
run: cargo clippy --release --workspace --features static
shell: bash
run: |
for target in ${TARGETS}; do
cargo android \
clippy --release --workspace --features static \
--target "${target}"
done
- name: Build
run: cargo build --release --workspace --features static
shell: bash
run: |
for target in ${TARGETS}; do
cargo android \
build --release --workspace --features static \
--target "${target}"
done
- name: Tests
run: cargo test --release --workspace --features static
shell: bash
run: |
for target in ${TARGETS}; do
cargo android \
test --release --workspace --features static \
--target "${target}"
done
- 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
- name: End to end tests
shell: bash
run: |
for target in ${TARGETS}; do
cargo android \
run --release -p e2e --features static \
--target "${target}" \
-- test -a -c e2e/e2e.toml
done
- name: Create output directory
shell: bash
run: |
rm -rf target/output
case "${{ matrix.artifact.combine }}" in
lipo)
mkdir target/output
cmd=(lipo -output target/output/avbroot -create)
for target in ${TARGETS}; do
cmd+=("target/${target}/release/avbroot")
done
"${cmd[@]}"
;;
'')
ln -s "${TARGETS}/release" target/output
;;
*)
echo >&2 "Unsupported combine argument"
exit 1
;;
esac
# 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/
# This is separate so we can have a flat directory structure.
- 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 }}
name: avbroot-${{ steps.get_version.outputs.version }}-${{ matrix.artifact.name }}
path: |
target/release/avbroot
target/release/avbroot.exe
- name: Cache e2e executable
uses: actions/cache@v3
with:
key: e2e-${{ github.sha }}-${{ runner.os }}
path: |
target/release/e2e
target/release/e2e.exe
setup:
name: Prepare workflow data
runs-on: ubuntu-latest
needs: build
timeout-minutes: 2
outputs:
config-path: ${{ steps.load-config.outputs.config-path }}
device-list: ${{ steps.load-config.outputs.device-list }}
magisk-key: ${{ steps.cache-keys.outputs.magisk-key }}
img-key-prefix: ${{ steps.cache-keys.outputs.img-key-prefix }}
img-hit: ${{ steps.get-img-cache.outputs.cache-matched-key }}
steps:
- uses: actions/checkout@v3
- name: Restore e2e executable
uses: actions/cache/restore@v3
with:
key: e2e-${{ github.sha }}-${{ runner.os }}
fail-on-cache-miss: true
path: |
target/release/e2e
target/release/e2e.exe
- name: Loading test config
id: load-config
working-directory: e2e
run: |
echo 'config-path=e2e/e2e.toml' >> "${GITHUB_OUTPUT}"
echo -n 'device-list=' >> "${GITHUB_OUTPUT}"
../target/release/e2e list \
| jq -cnR '[inputs | select(length > 0)]' \
>> "${GITHUB_OUTPUT}"
- name: Generating cache keys
id: cache-keys
run: |
{
echo "img-key-prefix=img-${{ hashFiles(steps.load-config.outputs.config-path) }}-"; \
echo "magisk-key=magisk-${{ hashFiles(steps.load-config.outputs.config-path) }}";
} >> $GITHUB_OUTPUT
- name: Checking for cached device images
id: get-img-cache
uses: actions/cache/restore@v3
with:
key: ${{ steps.cache-keys.outputs.img-key-prefix }}
lookup-only: true
path: |
e2e/files/${{ fromJSON(steps.load-config.outputs.device-list)[0] }}-sparse.tar
- name: Checking for cached magisk apk
id: get-magisk-cache
uses: actions/cache/restore@v3
with:
key: ${{ steps.cache-keys.outputs.magisk-key }}
lookup-only: true
path: e2e/files/magisk
- name: Preloading Magisk cache
if: ${{ ! steps.get-magisk-cache.outputs.cache-hit }}
uses: ./.github/actions/preload-magisk-cache
with:
cache-key: ${{ steps.cache-keys.outputs.magisk-key }}
preload-img:
name: Preload device images
runs-on: ubuntu-latest
needs: setup
timeout-minutes: 5
# Assume that preloading always succesfully cached all images before.
# If for some reason only some got cached, on the first run, the cache will not be preloaded
# which will result in some being downloaded multiple times when running the tests.
if: ${{ ! needs.setup.outputs.img-hit }}
strategy:
matrix:
device: ${{ fromJSON(needs.setup.outputs.device-list) }}
steps:
- uses: actions/checkout@v3
- name: Preloading image cache
uses: ./.github/actions/preload-img-cache
with:
cache-key-prefix: ${{ needs.setup.outputs.img-key-prefix }}
device: ${{ matrix.device }}
tests:
name: Run test for ${{ matrix.device }} on ${{ matrix.os }}
runs-on: ubuntu-latest
needs:
- setup
- preload-img
timeout-minutes: 10
# Continue on skipped but not on failures or cancels
if: ${{ always() && ! failure() && ! cancelled() }}
strategy:
matrix:
device: ${{ fromJSON(needs.setup.outputs.device-list) }}
os:
- ubuntu-latest
- windows-latest
- macos-latest
steps:
- uses: actions/checkout@v3
- name: Restoring Magisk cache
uses: ./.github/actions/preload-magisk-cache
with:
cache-key: ${{ needs.setup.outputs.magisk-key }}
- name: Restoring image cache
uses: ./.github/actions/preload-img-cache
with:
cache-key-prefix: ${{ needs.setup.outputs.img-key-prefix }}
device: ${{ matrix.device }}
- name: Restore e2e executable
uses: actions/cache/restore@v3
with:
key: e2e-${{ github.sha }}-${{ runner.os }}
fail-on-cache-miss: true
path: |
target/release/e2e
target/release/e2e.exe
# Finally run tests
- name: Run test for ${{ matrix.device }}
working-directory: e2e
run: ../target/release/e2e test --stripped -d ${{ matrix.device }}
target/output/LICENSE
target/output/README.md
target/output/avbroot
target/output/avbroot.exe
+1 -1
View File
@@ -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
-35
View File
@@ -1,35 +0,0 @@
---
name: Modules
on:
push:
branches:
- master
pull_request:
jobs:
build:
name: Build modules
runs-on: ubuntu-latest
steps:
- name: Check out repository
uses: actions/checkout@v3
with:
# For git describe
fetch-depth: 0
- name: Get version
id: get_version
shell: bash
run: |
echo -n 'version=' >> "${GITHUB_OUTPUT}"
git describe --always \
| sed -E "s/^v//g;s/([^-]*-g)/r\1/;s/-/./g" \
>> "${GITHUB_OUTPUT}"
- name: Build modules
run: cargo xtask modules -a
- name: Archive artifacts
uses: actions/upload-artifact@v3
with:
name: avbroot-modules-${{ steps.get_version.outputs.version }}
path: modules/dist/
+2 -4
View File
@@ -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
+190
View File
@@ -7,6 +7,124 @@
to update the actual links at the bottom of the file.
-->
### Version 3.5.0
* Update all dependencies ([PR #329])
* Add new unpack and pack commands for `payload.bin` files ([Issue #328], [PR #331])
### Version 3.4.1
* Update all dependencies ([PR #321])
* Add support for Magisk 27006 ([PR #323])
### Version 3.4.0
* Fix (unreachable) minor error handling logic when attempting to use unsupported AVB signing algorithms ([PR #311])
* Add support for performing signing operations with external programs ([Issue #310], [PR #312])
* See the linked issue for an example of how to sign with a Yubikey.
### Version 3.3.0
* Recompute CoW size estimate when replacing dynamic partitions ([Issue #306], [PR #307])
* Fixes out of space error when flashing a patched OTA that uses `--replace` to replace a dynamic partition (eg. `system`) with a larger or more incompressible image
* Add `avbroot payload info` subcommand for inspecting `payload.bin` headers ([PR #309])
### Version 3.2.3
* Add prebuilt binary for Android (aarch64) ([PR #304])
### Version 3.2.2
* Add new `--recompute-size` option to `avbroot avb pack` to automatically recompute the image size for resizable images ([Discussion #294], [PR #296])
* Add new `--output-info` option to `avbroot avb pack` to write a new `avb.toml` file containing computed values ([PR #297])
* Add support for upcoming Magisk Canary 27003 ([Issue #301], [PR #268])
### Version 3.2.1
* Increase hash tree and FEC size limits to accommodate partition images up to 8 GiB ([Issue #291], [PR #293])
### Version 3.2.0
* Fix potential infinite loop when interrupting avbroot at the right moment to a bug in the bzip2-rs library ([Issue #285], [PR #287])
* Update all dependencies and fix new clippy lints ([PR #288])
* Add support for adding the custom AVB public key to the list of trusted keys for DSU (booting signed GSIs) ([Discussion #286], [PR #289])
### Version 3.1.3
* Build universal binary for macOS ([Issue #278], [PR #279])
### 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])
* https://github.com/chenxiaolong/OEMUnlockOnBoot
* The new module supports the automatic update mechanism within Magisk/KernelSU
* Add support for Magisk v27.0 ([PR #255])
* Switch to using a proper logging library ([PR #251])
* Folks who want to see the juicy details during patching can use `--log-level debug` or `--log-level trace`
Behind-the-scenes changes:
* Switch from xz2 to liblzma (maintained fork of xz2) ([PR #247])
* Update all dependencies ([PR #256])
### Version 3.0.0
Happy New Year! This release brings two major changes:
1. The OTA certificates (`otacerts.zip`) in the system partition are now patched. The `clearotacerts` module from avbroot (or the `customotacerts` module from Custota) are no longer needed and can be safely uninstalled.
This makes it possible to use Pixel's new Repair Mode safely. To do so, follow the instructions in the [documentation here](./README.md#repair-mode).
2. Autodetection for boot partitions is now significantly more reliable. For KernelSU users or folks who have more obscure devices, the `--boot-partition` option is no longer required (and is now ignored).
Full list of changes:
* Add support for AVB 2.0 format 1.3.0 (for Android 15) ([PR #210])
* Add new `avbroot key decode-avb` command for converting AVB-encoded public keys to the standard PKCS8-encoded format ([PR #219])
* Improve autodetection of boot images ([Issue #218], [PR #221], [PR #237])
* Build precompiled executables as statically linked executables ([Issue #222], [PR #224], [PR #227])
* Limit critical partition check to bootloader-verified partitions ([Issue #223], [PR #226])
* Improve patching performance by spliiting new partition images into chunks and compressing them in parallel ([PR #228])
* Also verify whole-partition hashes when running `avbroot ota verify` ([PR #229])
* Add support for patching `otacerts.zip` on the system partition ([Issue #225], [PR #240], [PR #244])
* Document how to use Repair Mode safely ([Issue #216], [PR #243])
Behind-the-scenes changes:
* Fix lint warnings introduced in Rust 1.74.0 ([PR #211])
* Temporarily silence [RUSTSEC-2023-0071](https://rustsec.org/advisories/RUSTSEC-2023-0071) warning in cargo-deny ([PR #214])
* Add support for partially updating FEC data ([PR #230], [PR #231], [PR #234])
* Fix hash tree calculation for images smaller than one block ([PR #232])
* Refactor hash tree code and add tests, CLI commands, and support for partial updates ([PR #233])
* Generate mock OTAs to use for end-to-end tests ([PR #241])
* Update all dependencies ([PR #245])
### Version 2.3.3
* Add support for XZ-compressed ramdisks ([Issue #203], [PR #207])
* Merge property and kernel command line AVB descriptors when replacing partitions ([Issue #203], [PR #208])
### Version 2.3.2
* Improve error messages when using `--replace` with an image that has the wrong AVB descriptor type ([Issue #201], [PR #202])
* Automatically update legacy `dm=` kernel command line descriptor when packing AVB images ([Issue #203], [PR #205])
* Automatically promote insecure hash algorithms (eg. sha1) to sha256 when packing AVB images ([Issue #203], [PR #206])
### Version 2.3.1
* Mark Magisk 264xx as supported ([PR #199])
### Version 2.3.0
* Fix missing `--help` text for `avbroot avb unpack`'s `--ignore-invalid` option ([PR #183])
@@ -93,6 +211,9 @@ Behind-the-scenes changes:
<!-- Do not manually edit the lines below. Use `cargo xtask update-changelog` to regenerate. -->
[Discussion #195]: https://github.com/chenxiaolong/avbroot/discussions/195
[Discussion #235]: https://github.com/chenxiaolong/avbroot/discussions/235
[Discussion #286]: https://github.com/chenxiaolong/avbroot/discussions/286
[Discussion #294]: https://github.com/chenxiaolong/avbroot/discussions/294
[Issue #138]: https://github.com/chenxiaolong/avbroot/issues/138
[Issue #144]: https://github.com/chenxiaolong/avbroot/issues/144
[Issue #145]: https://github.com/chenxiaolong/avbroot/issues/145
@@ -100,6 +221,21 @@ Behind-the-scenes changes:
[Issue #157]: https://github.com/chenxiaolong/avbroot/issues/157
[Issue #160]: https://github.com/chenxiaolong/avbroot/issues/160
[Issue #166]: https://github.com/chenxiaolong/avbroot/issues/166
[Issue #201]: https://github.com/chenxiaolong/avbroot/issues/201
[Issue #203]: https://github.com/chenxiaolong/avbroot/issues/203
[Issue #216]: https://github.com/chenxiaolong/avbroot/issues/216
[Issue #218]: https://github.com/chenxiaolong/avbroot/issues/218
[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
[Issue #278]: https://github.com/chenxiaolong/avbroot/issues/278
[Issue #285]: https://github.com/chenxiaolong/avbroot/issues/285
[Issue #291]: https://github.com/chenxiaolong/avbroot/issues/291
[Issue #301]: https://github.com/chenxiaolong/avbroot/issues/301
[Issue #306]: https://github.com/chenxiaolong/avbroot/issues/306
[Issue #310]: https://github.com/chenxiaolong/avbroot/issues/310
[Issue #328]: https://github.com/chenxiaolong/avbroot/issues/328
[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
@@ -143,3 +279,57 @@ Behind-the-scenes changes:
[PR #191]: https://github.com/chenxiaolong/avbroot/pull/191
[PR #196]: https://github.com/chenxiaolong/avbroot/pull/196
[PR #197]: https://github.com/chenxiaolong/avbroot/pull/197
[PR #199]: https://github.com/chenxiaolong/avbroot/pull/199
[PR #202]: https://github.com/chenxiaolong/avbroot/pull/202
[PR #205]: https://github.com/chenxiaolong/avbroot/pull/205
[PR #206]: https://github.com/chenxiaolong/avbroot/pull/206
[PR #207]: https://github.com/chenxiaolong/avbroot/pull/207
[PR #208]: https://github.com/chenxiaolong/avbroot/pull/208
[PR #210]: https://github.com/chenxiaolong/avbroot/pull/210
[PR #211]: https://github.com/chenxiaolong/avbroot/pull/211
[PR #214]: https://github.com/chenxiaolong/avbroot/pull/214
[PR #219]: https://github.com/chenxiaolong/avbroot/pull/219
[PR #221]: https://github.com/chenxiaolong/avbroot/pull/221
[PR #224]: https://github.com/chenxiaolong/avbroot/pull/224
[PR #226]: https://github.com/chenxiaolong/avbroot/pull/226
[PR #227]: https://github.com/chenxiaolong/avbroot/pull/227
[PR #228]: https://github.com/chenxiaolong/avbroot/pull/228
[PR #229]: https://github.com/chenxiaolong/avbroot/pull/229
[PR #230]: https://github.com/chenxiaolong/avbroot/pull/230
[PR #231]: https://github.com/chenxiaolong/avbroot/pull/231
[PR #232]: https://github.com/chenxiaolong/avbroot/pull/232
[PR #233]: https://github.com/chenxiaolong/avbroot/pull/233
[PR #234]: https://github.com/chenxiaolong/avbroot/pull/234
[PR #237]: https://github.com/chenxiaolong/avbroot/pull/237
[PR #240]: https://github.com/chenxiaolong/avbroot/pull/240
[PR #241]: https://github.com/chenxiaolong/avbroot/pull/241
[PR #243]: https://github.com/chenxiaolong/avbroot/pull/243
[PR #244]: https://github.com/chenxiaolong/avbroot/pull/244
[PR #245]: https://github.com/chenxiaolong/avbroot/pull/245
[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 #268]: https://github.com/chenxiaolong/avbroot/pull/268
[PR #276]: https://github.com/chenxiaolong/avbroot/pull/276
[PR #277]: https://github.com/chenxiaolong/avbroot/pull/277
[PR #279]: https://github.com/chenxiaolong/avbroot/pull/279
[PR #287]: https://github.com/chenxiaolong/avbroot/pull/287
[PR #288]: https://github.com/chenxiaolong/avbroot/pull/288
[PR #289]: https://github.com/chenxiaolong/avbroot/pull/289
[PR #293]: https://github.com/chenxiaolong/avbroot/pull/293
[PR #296]: https://github.com/chenxiaolong/avbroot/pull/296
[PR #297]: https://github.com/chenxiaolong/avbroot/pull/297
[PR #304]: https://github.com/chenxiaolong/avbroot/pull/304
[PR #307]: https://github.com/chenxiaolong/avbroot/pull/307
[PR #309]: https://github.com/chenxiaolong/avbroot/pull/309
[PR #311]: https://github.com/chenxiaolong/avbroot/pull/311
[PR #312]: https://github.com/chenxiaolong/avbroot/pull/312
[PR #321]: https://github.com/chenxiaolong/avbroot/pull/321
[PR #323]: https://github.com/chenxiaolong/avbroot/pull/323
[PR #329]: https://github.com/chenxiaolong/avbroot/pull/329
[PR #331]: https://github.com/chenxiaolong/avbroot/pull/331
Generated
+578 -609
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -4,7 +4,7 @@ members = ["avbroot", "e2e", "fuzz", "xtask"]
resolver = "2"
[workspace.package]
version = "2.3.0"
version = "3.5.0"
license = "GPL-3.0-only"
edition = "2021"
repository = "https://github.com/chenxiaolong/avbroot"
+100
View File
@@ -30,6 +30,12 @@ This subcommand packs a new AVB image from the `avb.toml` file and, for appended
* To force an image to be signed, use `--key <path> --force`.
* To force an image to be unsigned, use `--force` without specifying `--key`.
Note that if the image is an appended image and its hash or hash tree descriptor uses an insecure algorithm, like `sha1`, then it will automatically be promoted to `sha256`.
By default, for appended vbmeta images, the output image size will match the size of the original image that was unpacked. This size is specified by the `image_size` field in `avb.toml`. If the image is resizable (eg. `system`), then passing in `--recompute-size` will cause the `image_size` field to be ignored and the smallest possible output file that fits the raw image and AVB metadata will be built. This avoids wasting space if `raw.img` shrunk or allows the packing to work at all if `raw.img` grew. **Do not use this option for non-resizable images** (eg. `boot`) or else the device won't be able to boot.
When packing an image, several of the fields in `avb.toml` may potentially be recomputed. To write a TOML file containing the new values, use `--output-info <output TOML>`. It is safe to overwrite the existing `avb.toml` if desired.
### Repacking an AVB image
```bash
@@ -173,6 +179,14 @@ The default behavior is to use 2 bytes of parity information per 253 bytes of in
The number of parity bytes (between 2 and 24, inclusive) can be configured using `--parity`.
### Updating FEC data
```bash
avbroot fec update -i <input data file> -f <FEC file> [-r <start> <end>]...
```
This will update the FEC data corresponding to the specified regions. This can be significantly faster than generating new FEC data from scratch for large files if the regions where data was modified are known.
### Verifying a file
```bash
@@ -192,3 +206,89 @@ avbroot fec repair -i <input/output data file> -f <input FEC file>
This will repair the file in place. As described above, in each column, up to `parity / 2` bytes can be corrected.
Note that FEC is **not** a replacement for checksums, like SHA-256. When there are too many errors, the file can potentially be "successfully repaired" to some incorrect data.
## `avbroot hash-tree`
This set of commands is for working with dm-verity hash tree data. They are not especially useful outside of debugging avbroot itself because the output format is custom. There is a custom header that sits in front of the standard dm-verity hash tree data.
| Offsets | Type | Description |
|------------|--------|--------------------------------|
| 0..16 | ASCII | `avbroot!hashtree` magic bytes |
| 16..18 | U16LE | Version (currently 1) |
| 18..26 | U64LE | Image size |
| 26..30 | U32LE | Block size |
| 30..46 | ASCII | Hash algorithm |
| 46..48 | U16LE | Salt size |
| 48..50 | U16LE | Root digest size |
| 50..54 | U32LE | Hash tree size |
| (Variable) | BINARY | Salt |
| (Variable) | BINARY | Root digest |
| (Variable) | BINARY | Hash tree |
For more information on the hash tree data, see the [Linux kernel documentation](https://docs.kernel.org/admin-guide/device-mapper/verity.html#hash-tree) or avbroot's implementation in [`hashtree.rs`](./avbroot/src/format/hashtree.rs).
### Generating hash tree
```bash
avbroot hash-tree generate -i <input data file> -H <output hash tree file>
```
The default behavior is to use a block size of 4096, the `sha256` algorithm, and an empty salt. These can be changed with the `-b`, `-a`, and `-s` options, respectively.
All parameters needed for verification are included in the hash tree file's header.
### Updating hash tree
```bash
avbroot hash-tree update -i <input data file> -H <hash tree file> [-r <start> <end>]...
```
This will update the hash tree data corresponding to the specified regions. This can be significantly faster than generating new hash tree data from scratch for large files if the regions where data was modified are known.
### Verifying a file
```bash
avbroot hash-tree verify -i <input data file> -H <input hash tree file>
```
This will check if the input file has any corrupted blocks. Currently, the command cannot report which specific blocks are corrupted, only whether the file is valid.
## `avbroot payload`
### Unpacking a payload binary
```bash
avbroot payload unpack -i <input payload>
```
This subcommand unpacks the payload header information to `payload.toml` and the partition images to the `payload_images` directory.
Only full payload binaries can be unpacked. Delta payload binaries from incremental OTAs are not supported.
### Packing a payload binary
```bash
avbroot payload pack -o <output payload> --key <OTA private key>
```
This subcommand packs a new payload binary from the `payload.toml` file and `payload_images` directory. Any `.img` files in the `payload_images` directory that don't have a corresponding entry in `payload.toml` are silently ignored.
Packing a payload binary requires compressing all of the partition images, which is very CPU intensive. If re-signing an existing payload binary without making any other modifications is all that's needed, use the `repack` subcommand instead.
### Repacking a payload binary
```bash
avbroot payload repack -i <input payload> -o <output payload> --key <OTA private key>
```
This subcommand is logically equivalent to `avbroot payload unpack` followed by `avbroot payload pack`, except significantly more efficient. Instead of decompressing and recompressing all partition images, the raw data is directly copied from the input payload binary.
This is useful for re-signing a payload binary without making any other changes.
### Showing payload header information
```bash
avbroot payload info -i <payload>
```
This subcommand shows all of the payload header fields (which will likely be extremely long).
+211 -99
View File
@@ -1,48 +1,117 @@
# avbroot
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.
(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.
**NOTE:** avbroot 2.0 has been rewritten in Rust and no longer relies on any AOSP code. The CLI is fully backwards compatible, but the old Python implementation can be found in the `python` branch if needed.
## Requirements
* Only devices that use modern A/B partitioning are supported. This is the case for most non-Samsung devices launched with Android 10 or newer. To check if a device uses this partitioning scheme, open the OTA zip file and check that:
* `payload.bin` exists
* `META-INF/com/android/metadata` (Android 10-11) or `META-INF/com/android/metadata.pb` (Android 12+) exists
* The device must support using a custom public key for the bootloader's root of trust. This is normally done via the `fastboot flash avb_custom_key` command.
A list of devices known to work can be found in the issue tracker at [#299](https://github.com/chenxiaolong/avbroot/issues/299).
## Patches
avbroot applies two patches to the boot images:
avbroot applies the following patches to the partition images:
* Magisk is applied to the `boot` or `init_boot` image, depending on device, as if it were done from the Magisk app.
* The `boot` or `init_boot` image, depending on device, is patched to enable root access. For Magisk, the patch is equivalent to what would be normally done by the Magisk app.
* 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 after the bootloader has been locked. It also prevents accidental flashing of the original OTA package while booted into recovery.
* 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` 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
* The device must use modern (non-legacy-SAR) A/B partitioning. This is the case on newer Pixel and OnePlus devices. To check if a device uses this partitioning scheme, open the OTA zip file and check that:
* **Always leave the `OEM unlocking` checkbox enabled when using a locked bootloader with root.** This is critically important. Root access allows the boot partition to potentially be overwritten, either accidentally or intentionally, with an image that is not properly signed. In this scenario, if the checkbox is turned off, both the OS and recovery mode will be made unbootable and `fastboot flashing unlock` will not be allowed. This effectively renders the device **_hard bricked_**.
* `payload.bin` exists
* `META-INF/com/android/metadata` (Android 11) or `META-INF/com/android/metadata.pb` (Android 12+) exists
Repeat: **_ALWAYS leave `OEM unlocking` enabled if rooted._**
* The device must support using a custom public key for the bootloader's root of trust. This is normally done via the `fastboot flash avb_custom_key` command. All Pixel devices with unlockable bootloaders support this, as well as most OnePlus devices. Other devices may support it as well, but there's no easy way to check without just trying it.
* Any operation that causes an improperly-signed boot image to be flashed will result in the device being unbootable and unrecoverable without unlocking the bootloader again (and thus, triggering a data wipe). A couple ways an improperly-signed boot image could be flashed include:
* **Do not ever disable the `OEM unlocking` checkbox when using a locked bootloader with root.** This is critically important. With root access, it is possible to corrupt the running system, for example by zeroing out the boot partition. In this scenario, if the checkbox is turned off, both the OS and recovery mode will be made unbootable and `fastboot flashing unlock` will not be allowed. This effectively renders the device **_hard bricked_**.
* The `Direct install` method for updating Magisk. Magisk updates **must** be done by repatching the OTA, not via the app.
* Any operation that causes an unsigned or differently-signed boot image to be flashed will result in the device being unbootable and unrecoverable without unlocking the bootloader again (and thus, triggering a data wipe). This includes:
* The `Uninstall Magisk` feature in Magisk. If root access is no longer needed, Magisk **must** be removed by repatching the OTA with the `--rootless` option, not via the app.
* Performing a regular (unpatched) A/B OTA update. This can be blocked via a Magisk module (see: [Blocking A/B OTA Updates](#blocking-ab-ota-updates)).
If the boot image is ever modified, **do not reboot**. [Open an issue](https://github.com/chenxiaolong/avbroot/issues/new) for support and be very clear about what steps were done that lead to the situation. If Android is still running and root access works, it might be possible to recover without wiping and starting over.
* The `Direct install` method for updating Magisk. Magisk updates must be done by repatching as well.
## Usage
1. Make sure the [caveats listed above](#warnings-and-caveats) are understood. It is possible to hard brick by doing the wrong thing!
2. Download the latest version from the [releases page](https://github.com/chenxiaolong/avbroot/releases). To verify the digital signature, see the [verifying digital signatures](#verifying-digital-signatures) section.
avbroot is a standalone executable. It does not need to be installed and can be run from anywhere.
3. Follow the steps to [generate signing keys](#generating-keys).
4. Patch the OTA zip. The base command is:
```bash
avbroot ota patch \
--input /path/to/ota.zip \
--key-avb /path/to/avb.key \
--key-ota /path/to/ota.key \
--cert-ota /path/to/ota.crt \
```
Add the following additional arguments to the end of the command depending on how you want to configure root access.
* To enable root access with Magisk:
```bash
--magisk /path/to/magisk.apk \
--magisk-preinit-device <name>
```
If you don't know the Magisk preinit partition name, see the [Magisk preinit device section](#magisk-preinit-device) for steps on how to find it.
If you prefer to manually patch the boot image via the Magisk app instead of letting avbroot handle it, use the following arguments instead:
```bash
--prepatched /path/to/magisk_patched-xxxxx_yyyyy.img
```
* To enable root access with KernelSU:
```bash
--prepatched /path/to/kernelsu/boot.img
```
* To leave the OS unrooted:
```bash
--rootless
```
For more details on the options above, see the [advanced usage section](#advanced-usage).
If `--output` is not specified, then the output file is written to `<input>.patched`.
5. The patched OTA is ready to go! To flash it for the first time, follow the steps in the [initial setup section](#initial-setup). For updates, follow the steps in the [updates section](#updates).
## Generating Keys
avbroot signs a few components while patching an OTA zip:
avbroot signs several components while patching an OTA zip:
* the root `vbmeta` image
* the boot image `vbmeta` footers (if the original ones were signed)
* the boot images
* the vbmeta images
* the OTA payload
* the OTA zip itself
The boot-related components are signed with an AVB key and OTA-related components are signed with an OTA key. They can be the same RSA keypair, though the following steps show how to generate two separate keys.
The first two components are signed with an AVB key and latter two components are signed with an OTA key. They can be the same key, though the following steps show how to generate two separate keys.
1. Generate the AVB and OTA signing keys:
When patching OTAs for multiple devices, generating unique keys for each device is strongly recommended because it prevents an OTA for the wrong device being accidentally flashed.
1. Generate the AVB and OTA signing keys.
```bash
avbroot key generate-key -o avb.key
@@ -55,94 +124,108 @@ The boot-related components are signed with an AVB key and OTA-related component
avbroot key extract-avb -k avb.key -o avb_pkmd.bin
```
3. Generate a self-signed certificate for the OTA signing key. This is used by recovery for verifying OTA updates.
3. Generate a self-signed certificate for the OTA signing key. This is used by recovery to verify OTA updates when sideloading.
```bash
avbroot key generate-cert -k ota.key -o ota.crt
```
The commands above are provided for convenience. avbroot is compatible with any standard PKCS8-encoded 4096-bit RSA private key and X509 certificate (eg. like those generated by openssl).
The commands above are provided for convenience. avbroot is compatible with any standard PKCS#8-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.
## Usage
## Initial setup
1. Make sure the caveats listed above are understood. It is possible to hard brick by doing the wrong thing!
2. Download the latest version from the [releases page](https://github.com/chenxiaolong/avbroot/releases). To verify the digital signature, see the [verifying digital signatures](#verifying-digital-signatures) section.
3. Follow the steps to [generate signing keys](#generating-keys).
4. Patch the full OTA ZIP.
1. Make sure that the version of fastboot is 34 or newer. Older versions have bugs that prevent the `fastboot flashall` command (required later) from working properly.
```bash
avbroot ota patch \
--input /path/to/ota.zip \
--key-avb /path/to/avb.key \
--key-ota /path/to/ota.key \
--cert-ota /path/to/ota.crt \
--magisk /path/to/magisk.apk
fastboot --version
```
If `--output` is not specified, then the output file is written to `<input>.patched`.
2. Reboot into fastboot mode and unlock the bootloader if it isn't already unlocked. This will trigger a data wipe.
**NOTE:** If you are using Magisk version >=25211, you need to know the preinit partition name (`--magisk-preinit-device <name>`). For details, see the [Magisk preinit device section](#magisk-preinit-device).
```bash
fastboot flashing unlock
```
If you prefer to use an existing boot image patched by the Magisk app or you want to use KernelSU, see the [advanced usage section](#advanced-usage).
3. When setting things up for the first time, the device must already be running the correct OS. Flash the original unpatched OTA if needed.
5. **[Initial setup only]** Unlock the bootloader. This will trigger a data wipe.
6. **[Initial setup only]** Extract the patched images from the patched OTA.
4. Extract the partition images from the patched OTA that are different from the original.
```bash
avbroot ota extract \
--input /path/to/ota.zip.patched \
--directory extracted
--directory extracted \
--fastboot
```
7. **[Initial setup only]** Flash the patched images and the AVB public key metadata. This sets up the custom root of trust. Future updates are done by simply sideloading patched OTA zips.
If you prefer to extract and flash all OS partitions just to be safe, pass in `--all`.
5. Flash the partition images that were extracted.
```bash
# Flash the boot images that were extracted
for image in extracted/*.img; do
partition=$(basename "${image}")
partition=${partition%.img}
ANDROID_PRODUCT_OUT=extracted fastboot flashall --skip-reboot
```
fastboot flash "${partition}" "${image}"
done
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.
# Flash the AVB signing public key
Alternatively, for Pixel devices, running `flash-base.sh` from the factory image will also update the bootloader and modem.
6. 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
```
8. **[Initial setup only]** Run `dmesg | grep libfs_avb` as root to verify that AVB is working properly. A message similar to the following is expected:
7. **[Optional]** Before locking the bootloader, reboot into Android once to confirm that everything is properly signed.
Install the Magisk or KernelSU app and run the following command:
```bash
adb shell su -c 'dmesg | grep libfs_avb'
```
If AVB is working properly, the following message should be printed out:
```bash
init: [libfs_avb]Returning avb_handle with status: Success
```
9. **[Initial setup only]** Lock the bootloader. This will trigger a data wipe again. **Do not uncheck `OEM unlocking`!**
8. Reboot back into fastboot and lock the bootloader. This will trigger a data wipe again.
**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 [avbroot's `oemunlockonboot` Magisk module](#oemunlockonboot-enable-oem-unlocking-on-every-boot) to automatically ensure OEM unlocking is enabled on every boot.
```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.
9. That's it! To install future OS, Magisk, or KernelSU updates, see the [next section](#updates).
## Updates
To update Android or Magisk:
Updates to Android, Magisk, and KernelSU are all done the same way by patching (or repatching) the OTA.
1. Follow step 4 in [the previous section](#usage) to patch the new OTA (or an existing OTA with a newer Magisk APK).
1. If Magisk or KernelSU is being updated, first install their new `.apk`. If you happen to open the app, make sure it **does not** flash the boot image. Cancel the boot image update prompts if needed.
2. Reboot to recovery mode. If stuck at a `No command` screen, press the volume up button once while holding down the power button.
2. Follow the step in the [usage section](#usage) to patch the new OTA.
3. Sideload the patched OTA.
3. Reboot to recovery mode. If the screen is stuck at a `No command` message, press the volume up button once while holding down the power button.
4. Reboot.
4. Sideload the patched OTA with `adb sideload`.
5. That's it!
## Reverting to stock firmware
To stop using avbroot and revert to the stock firmware:
1. Unlock the bootloader. This will trigger a data wipe.
1. Reboot into fastboot mode and unlock the bootloader. This will trigger a data wipe.
2. Erase the custom AVB public key.
@@ -152,35 +235,42 @@ To stop using avbroot and revert to the stock firmware:
3. Flash the stock firmware.
## avbroot Magisk modules
4. That's it! There are no other remnants to clean up.
avbroot's Magisk modules can be found on the [releases page](https://github.com/chenxiaolong/avbroot/releases) or they can be built locally by running:
## OTA updates
```bash
cargo xtask modules -a
```
avbroot replaces `/system/etc/security/otacerts.zip` in both the system and recovery partitions with a new zip that contains the custom OTA signing certificate. This prevents an unpatched OTA from inadvertently being installed both when booted into Android and when sideloading from recovery.
This requires Java and the Android SDK to be installed. The `ANDROID_HOME` environment variable should be set to the Android SDK path.
Disabling the system updater app is recommended to prevent it from even attempting to install an unpatched OTA. To do so:
### `clearotacerts`: Blocking A/B OTA Updates
* Stock OS: Turn off `Automatic system updates` in Android's Developer Options.
* Custom OS: Disable the system updater app (or block its network access) from Settings -> Apps -> See all apps -> (three-dot menu) -> Show system -> (find updater app).
Unpatched OTA updates are already blocked in recovery because the original OTA certificate has been replaced with the custom certificate. To disable automatic OTAs while booted into Android, turn off `Automatic system updates` in Android's Developer Options.
This is especially important for some custom OS's because their system updater app may get stuck in an infinite loop downloading an OTA update and then retrying when signature verification fails.
The `clearotacerts` module additionally makes A/B OTAs fail while booted into Android to prevent accidental manual updates. The module simply overrides `/system/etc/security/otacerts.zip` at runtime with an empty zip so that even if an OTA is downloaded, signature verification will fail.
To self-host a custom OTA server, see [Custota](https://github.com/chenxiaolong/Custota).
At least in CalyxOS, the Updater app does not respect the `Automatic system updates` setting and may enter an infinite loop downloading the OTA update and restarting the download when signature verification fails. If this happens on your ROM, you can try to either remove network access from the Updater app or disable the Updater app altogether (if your ROM allows you to do so). In CalyxOS, it is possible to go to `Settings > Apps > See all apps`, open the three-dot menu, `Show system`, then find the `System updater` app and disable it.
## Repair mode
Alternatively, see [Custota](https://github.com/chenxiaolong/Custota) for a custom OTA updater app that pulls from a self-hosted OTA server.
Some devices now ship with a Repair Mode feature that boots the system with a fresh `userdata` image so that repair technicians are able to run on-device diagnostics without needing the user's credentials to unlock the device.
### `oemunlockonboot`: Enable OEM unlocking on every boot
When the device is rooted, it is unsafe to use Repair Mode. Unless you are using release builds of Magisk/KernelSU signed with your own keys, it's trivial for someone to just install the Magisk/KernelSU app while in repair mode to gain root access with no authentication.
To help reduce the risk of OEM unlocking being accidentally disabled (or intentionally disabled as part of some OS's initial setup wizard), this module will attempt to enable the OEM unlocking option on every boot.
To safely use Repair Mode:
The logs for this module can be found at `/data/local/tmp/avbroot_oem_unlock.log`.
1. Unroot the device by repatching the OTA with the `--rootless` option (instead of `--magisk` or `--prepatched`) and flashing it.
2. Turn on Repair Mode.
3. After receiving the repaired device, exit Repair Mode.
4. Flash the (rooted) patched OTA as normal.
Because the unrooting and rooting are done by flashing OTAs, the device's data will not be wiped.
## Magisk preinit device
Magisk versions 25211 and newer require a writable partition for storing custom SELinux rules that need to be accessed during early boot stages. This can only be determined on a real device, so avbroot requires the partition's block device name to be specified via `--magisk-preinit-device <name>`. To find the partition name:
Magisk versions 25211 and newer require a writable partition for storing custom SELinux rules that need to be accessed during early boot stages. This can only be determined on a real device, so avbroot requires the partition to be explicitly specified via `--magisk-preinit-device <name>`. To find the partition name:
1. Extract the boot image from the original/unpatched OTA:
@@ -191,9 +281,9 @@ Magisk versions 25211 and newer require a writable partition for storing custom
--boot-only
```
2. Patch the boot image via the Magisk app. This **MUST** be done on the target device! The partition name will be incorrect if patched from Magisk on a different device.
2. Patch the boot image via the Magisk app. This **MUST** be done on the target device or a device of the same model! The partition name will be incorrect if patched from Magisk on a different device model.
The Magisk app will include a line like the following in the output:
The Magisk app will print out a line like the following in the output:
```
- Pre-init storage partition device ID: <name>
@@ -223,6 +313,8 @@ avbroot ota verify \
--public-key-avb /path/to/avb_pkmd.bin
```
This command works for any OTA, regardless if it's patched or unpatched.
If the `--cert-ota` and `--public-key-avb` options are omitted, then the signatures are only checked for validity, not that they are trusted.
## Tab completion
@@ -265,15 +357,13 @@ Invoke-Expression (& avbroot completion -s powershell)
### Using a prepatched boot image
avbroot can replace the boot image with a prepatched image instead of applying the Magisk root patch itself. This is useful for using a boot image patched by the Magisk app or for KernelSU. To use a prepatched boot image, pass in `--prepatched <boot image>` instead of `--magisk <apk>`. When using `--prepatched`, avbroot will skip applying the Magisk root patch, but will still apply the OTA certificate patch.
For KernelSU, also pass in `--boot-partition @gki_kernel` for both the `patch` and `extract` commands. avbroot defaults to Magisk's semantics where the boot image containing the GKI ramdisk is needed, whereas KernelSU requires the boot image containing the GKI kernel. This only affects devices launching with Android 13, where the GKI kernel and ramdisk are in different partitions (`boot` vs. `init_boot`), but it is safe and recommended to always use this option for KernelSU.
avbroot can replace the boot image with a prepatched image instead of applying the root patch itself. This is useful for using a boot image patched by the Magisk app or for KernelSU. To use a prepatched Magisk boot image or a KernelSU boot image, pass in `--prepatched <boot image>` instead of `--magisk <apk>`. When using `--prepatched`, avbroot will skip applying the Magisk root patch, but will still apply the OTA certificate patch.
Note that avbroot will validate that the prepatched image is compatible with the original. If, for example, the header fields do not match or a boot image section is missing, then the patching process will abort. The checks are not foolproof, but should help protect against accidental use of the wrong boot image. To bypass a somewhat "safe" subset of the checks, use `--ignore-prepatched-compat`. To ignore all checks (strongly discouraged!), pass it in twice.
### Skipping root patches
avbroot can be used for just resigning an OTA by specifying `--rootless` instead of `--magisk`/`--prepatched`. With this option, the patched OTA will not be rooted. The only modification applied is the replacement of the OTA verification certificate so that the OS can be upgraded with future (patched) OTAs.
avbroot can be used for just re-signing an OTA by specifying `--rootless` instead of `--magisk`/`--prepatched`. With this option, the patched OTA will not be rooted. The only modification applied is the replacement of the OTA verification certificate so that the OS can be upgraded with future (patched) OTAs.
### Replacing partitions
@@ -283,12 +373,18 @@ The only behavior this changes is where the partition is read from. When using `
This has no impact on what patches are applied. For example, when using Magisk, the root patch is applied to the boot partition, no matter if the partition came from the original `payload.bin` or from `--replace`.
### Booting signed GSIs
Android's [Dynamic System Updates (DSU)](https://developer.android.com/topic/dsu) feature uses a different root of trust than the regular system. Instead of using the bootloader's `avb_custom_key`, it obtains the trusted keys from the `first_stage_ramdisk/avb/*.avbpubkey` files inside the `init_boot` or `vendor_boot` ramdisk. These files are encoded in the same binary format as `avb_pkmd.bin`.
avbroot can add the custom AVB public key to this directory by passing in `--dsu` when patching an OTA. This allows booting [Generic System Images (GSI)](https://developer.android.com/topic/generic-system-image) signed by the custom AVB key.
### Clearing vbmeta flags
Some Android builds may ship with a root `vbmeta` image with the flags set such that AVB is effectively disabled. When avbroot encounters these images, the patching process will fail with a message like:
```
ValueError: vbmeta flags disable AVB: 0x3
Verified boot is disabled by vbmeta's header flags: 0x3
```
To forcibly enable AVB (by clearing the flags), pass in `--clear-vbmeta-flags`.
@@ -297,7 +393,7 @@ To forcibly enable AVB (by clearing the flags), pass in `--clear-vbmeta-flags`.
avbroot prompts for the private key passphrases interactively by default. To run avbroot non-interactively, either:
* Supply the passphrases via files:
* Supply the passphrases via files.
```bash
avbroot ota patch \
@@ -340,6 +436,28 @@ avbroot ota extract \
--all
```
### Signing with an external program
avbroot supports delegating all RSA signing operations to an external program with the `--signing-helper` option. When using this option, the `--key-avb` and `--key-ota` options must be given a public key instead of a private key.
For each signing operation, avbroot will invoke the program with:
```bash
<helper> <algorithm> <public key>
```
The algorithm is one of `SHA{256,512}_RSA{2048,4096}` and the public key is what was passed to avbroot. The program can use the public key to find the corresponding private key (eg. on a hardware security module). avbroot will write a PKCS#1 v1.5 padded digest to `stdin` and the helper program is expected to perform a raw RSA signing operation and write the raw signature (octet string matching key size) to `stdout`.
By default, this behavior is compatible with the `--signing_helper` option in AOSP's avbtool. However, avbroot additionally extends the arguments to support non-interactive use. If `--pass-{avb,ota}-file` or `--pass-{avb,ota}-env-var` are used, then the helper program will be invoked with two additional arguments that point to the password file or environment variable.
```bash
<helper> <algorithm> <public key> file <pass file>
# or
<helper> <algorithm> <public key> env <env file>
```
Note that avbroot will verify the signature returned by helper program against the public key. This ensures that the patching process will fail appropriately if the wrong private key was used.
## Building from source
Make sure the [Rust toolchain](https://www.rust-lang.org/) is installed. Then run:
@@ -352,27 +470,21 @@ The output binary is written to `target/release/avbroot`.
Debug builds work too, but they will run significantly slower (in the sha256 computations) due to compiler optimizations being turned off.
By default, the build links to the system's bzip2 and liblzma libraries, which are the only external libraries avbroot depends on. To compile and statically link these two libraries, pass in `--features static`.
By default, the executable links to the system's bzip2 and liblzma libraries, which are the only external libraries avbroot depends on. To compile and statically link these two libraries, pass in `--features static`.
### Android cross-compilation
To cross-compile for Android, install [cargo-android](https://github.com/chenxiaolong/cargo-android) and use the `cargo android` wrapper. To make a release build for aarch64, run:
```bash
cargo android build --release --target aarch64-linux-android
```
It is possible to run the tests if the host is running Linux, qemu-user-static is installed, and the executable is built with `RUSTFLAGS=-C target-feature=+crt-static` and `--features static`.
## Verifying digital signatures
First, save the public key to a file listing the keys to be trusted.
```bash
echo 'avbroot ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIDOe6/tBnO7xZhAWXRj3ApUYgn+XZ0wnQiXM8B7tPgv4' > avbroot_trusted_keys
```
Then, verify the signature of the zip file using the list of trusted keys.
```bash
ssh-keygen -Y verify -f avbroot_trusted_keys -I avbroot -n file -s <file>.zip.sig < <file>.zip
```
If the file is successfully verified, the output will be:
```
Good "file" signature for avbroot with ED25519 key SHA256:Ct0HoRyrFLrnF9W+A/BKEiJmwx7yWkgaW/JvghKrboA
```
To verify the digital signatures of the downloads, follow [the steps here](https://github.com/chenxiaolong/chenxiaolong/blob/master/VERIFY_SSH_SIGNATURES.md).
## Contributing
+470
View File
@@ -0,0 +1,470 @@
# 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`.
Список девайсов, на которых проверялась совместимость с указанным выше функционалом, находится здесь: [#299.](https://github.com/chenxiaolong/avbroot/issues/299)
## Патчи
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` в приложении Magisk. Если вам больше не нужен root-доступ, Magisk **должен быть удален путем репатчинга OTA** с использованием параметра `--rootless`, но не через его приложение.
Если в загрузочный раздел были внесены какие-либо изменения, **не перезагружайтесь**. Обратитесь за помощью, [открыв 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 версии 34 или новее. Предыдущие версии содержат баги, что не позволяют команде `fastboot flashall` (которая понадобится по ходу инструкции) работать правильно.
```bash
fastboot --version
```
2. Перезагрузитесь в режим fastboot и разблокируйте загрузчик, если не сделали этого ранее. Это приведет к стиранию всех пользовательских данных.
```bash
fastboot flashing unlock
```
3. Перед первой установкой, на устройстве уже должна быть установлена в оригинальном виде та прошивка, пропатченную версию которой вы собираетесь ставить. Если это не так, сначала установите оригинальную непропатченную OTA.
4. Извлекаем из пропатченного OTA модифицированные образы:
```bash
avbroot ota extract \
--input /путь/к/ota.zip.patched \
--directory extracted \
--fastboot
```
Если вы на всякий случай хотите прошить вообще все разделы из ОТА, извлечь их можно, указав аргумент `--all`.
5. Прошейте извлеченные образы разделов.
```bash
ANDROID_PRODUCT_OUT=extracted fastboot flashall --skip-reboot
```
Обратите внимание, что так прошиваются лишь те образы, что относятся к системе. Разделы загрузчика и модема же остаются нетронутыми из-за ограничений fastboot. Если они не обновлены до необходимой версии, или вы не уверены в этом, после прошивки перейдите к пункту [обновлений](#обновления) и установите пропатченный OTA-архив сайдлоадом в режиме Recovery. Прошивка полного OTA гарантирует, что абсолютно все разделы будут обновлены.
Для устройств Pixel есть ещё один вариант: запуск скрипта `flash-base.sh` из папки заводских образов (factory images) обновит загрузчик и модем.
6. После перезагрузки из fastbootd в загрузчик (bootloader), установите пользовательский публичный ключ AVB в загрузчик:
```bash
fastboot reboot-bootloader
fastboot erase avb_custom_key
fastboot flash avb_custom_key /путь/к/avb_pkmd.bin
```
7. **[Опционально]** Перед блокировкой загрузчика загрузитесь в систему, дабы убедиться, что все подписано правильно.
Установите приложение Magisk или KernelSU и выполните следующую команду:
```bash
adb shell su -c 'dmesg | grep libfs_avb'
```
Если AVB работает корректно, будет выведено следующее сообщение:
```bash
init: [libfs_avb]Returning avb_handle with status: Success
```
8. Перезагрузитесь в 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) который автоматически включает пункт Заводской разблокировки при каждом запуске системы.
9. Готово! Установка последующих обновлений системы, 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)
+18 -23
View File
@@ -10,11 +10,12 @@ publish = false
[dependencies]
anyhow = "1.0.75"
base64 = "0.21.3"
base64 = "0.22.1"
bitflags = "2.4.1"
bstr = "1.6.2"
byteorder = "1.4.3"
cap-std = "2.0.0"
cap-tempfile = "2.0.0"
cap-std = "3.0.0"
cap-tempfile = "3.0.0"
clap = { version = "4.4.1", features = ["derive"] }
clap_complete = "4.4.0"
cms = { version = "0.2.2", features = ["std"] }
@@ -23,13 +24,14 @@ ctrlc = "3.4.0"
flate2 = "1.0.27"
gf256 = { version = "0.3.0", features = ["rs"] }
hex = { version = "0.4.3", features = ["serde"] }
liblzma = "0.3.0"
lz4_flex = "0.11.1"
memchr = "2.6.0"
num-bigint-dig = "0.8.4"
num-traits = "0.2.16"
phf = { version = "0.11.2", features = ["macros"] }
pkcs8 = { version = "0.10.2", features = ["encryption", "pem"] }
prost = "0.12.1"
prost = "0.13.1"
rand = "0.8.5"
rayon = "1.7.0"
regex = { version = "1.9.4", default-features = false, features = ["perf", "std"] }
@@ -45,26 +47,18 @@ sha1 = "0.10.5"
sha2 = "0.10.7"
tempfile = "3.8.0"
thiserror = "1.0.47"
toml_edit = { version = "0.20.1", features = ["serde"] }
toml_edit = { version = "0.22.9", features = ["serde"] }
topological-sort = "0.2.2"
tracing = "0.1.40"
tracing-subscriber = "0.3.18"
x509-cert = { version = "0.2.4", features = ["builder"] }
# There's an upstream bug that causes an infinite loop in the write::BzDecoder
# destructor if the decoder is fed invalid data. While this never happens during
# normal operation, it is possible to run into this by running `ota extract`
# against a `--stripped` OTA file.
# https://github.com/alexcrichton/bzip2-rs/pull/99
# There are multiple upstream bugs that cause infinite loops in the Drop
# implementation of write::BzDecoder. Unfortunately, the project is no longer
# maintained, so we have to maintain our own fork with the necessary fixes.
[dependencies.bzip2]
git = "https://github.com/jongiddy/bzip2-rs"
rev = "2aefcb4d3634de1df226c73d93f758d65228bb8c"
# The upstream xz2 crate uses an old version of liblzma when compiling with the
# `static` feature and doesn't enable all of the encoders and decoders. This
# causes certain payload data to fail to decompress.
# https://github.com/chenxiaolong/avbroot/issues/138
[dependencies.xz2]
git = "https://github.com/chenxiaolong/xz2-rs"
rev = "fe2050b9c3395db15d8610f1dabb505440c1a556"
git = "https://github.com/chenxiaolong/bzip2-rs"
rev = "6e0f9836ec87b19261461b6cc1772e14aff8e851"
# https://github.com/zip-rs/zip/pull/383
[dependencies.zip]
@@ -77,11 +71,12 @@ features = ["deflate"]
rustix = { version = "0.38.9", default-features = false, features = ["process"] }
[build-dependencies]
prost-build = "0.12.1"
protox = "0.5.0"
constcat = "0.5.0"
prost-build = "0.13.1"
protox = "0.7.0"
[dev-dependencies]
assert_matches = "1.5.0"
[features]
static = ["bzip2/static", "xz2/static"]
static = ["bzip2/static", "liblzma/static"]
+55 -1
View File
@@ -1,5 +1,5 @@
/*
* SPDX-FileCopyrightText: 2023 Andrew Gunnerson
* SPDX-FileCopyrightText: 2023-2024 Andrew Gunnerson
* SPDX-License-Identifier: GPL-3.0-only
*/
@@ -30,8 +30,62 @@ fn main() {
let file_descriptors = protox::compile(&protos, [&in_dir]).unwrap();
const CUE_AI: &str = ".chromeos_update_engine.ApexInfo";
const CUE_DAM: &str = ".chromeos_update_engine.DeltaArchiveManifest";
const CUE_DPG: &str = ".chromeos_update_engine.DynamicPartitionGroup";
const CUE_DPM: &str = ".chromeos_update_engine.DynamicPartitionMetadata";
const CUE_PU: &str = ".chromeos_update_engine.PartitionUpdate";
const CUE_VABCFS: &str = ".chromeos_update_engine.VABCFeatureSet";
const DERIVE_SERDE: &str = "#[derive(serde::Deserialize, serde::Serialize)]";
const SERDE_DEFAULT: &str = "#[serde(default)]";
const SERDE_SKIP: &str = "#[serde(skip)]";
const SERDE_SKIP_IF_VEC_EMPTY: &str = "#[serde(skip_serializing_if = \"Vec::is_empty\")]";
use constcat::concat as c;
prost_build::Config::new()
.btree_map(["."])
// Allow deserializing and serializing the types we care about.
.type_attribute(CUE_AI, DERIVE_SERDE)
.type_attribute(CUE_DAM, DERIVE_SERDE)
.type_attribute(CUE_DPG, DERIVE_SERDE)
.type_attribute(CUE_DPM, DERIVE_SERDE)
.type_attribute(CUE_PU, DERIVE_SERDE)
.type_attribute(CUE_VABCFS, DERIVE_SERDE)
// Allow default-initializing all fields.
.type_attribute(CUE_AI, SERDE_DEFAULT)
.type_attribute(CUE_DAM, SERDE_DEFAULT)
.type_attribute(CUE_DPG, SERDE_DEFAULT)
.type_attribute(CUE_DPM, SERDE_DEFAULT)
.type_attribute(CUE_PU, SERDE_DEFAULT)
.type_attribute(CUE_VABCFS, SERDE_DEFAULT)
// Don't serialize fields that define the structure of the payload
// binary and that we recompute during packing.
.field_attribute(c!(CUE_DAM, ".signatures_offset"), SERDE_SKIP)
.field_attribute(c!(CUE_DAM, ".signatures_size"), SERDE_SKIP)
.field_attribute(c!(CUE_PU, ".operations"), SERDE_SKIP)
.field_attribute(c!(CUE_PU, ".estimate_cow_size"), SERDE_SKIP)
.field_attribute(c!(CUE_PU, ".old_partition_info"), SERDE_SKIP)
.field_attribute(c!(CUE_PU, ".new_partition_info"), SERDE_SKIP)
// Don't serialize AVB 1.0 fields.
.field_attribute(c!(CUE_PU, ".hash_tree_data_extent"), SERDE_SKIP)
.field_attribute(c!(CUE_PU, ".hash_tree_extent"), SERDE_SKIP)
.field_attribute(c!(CUE_PU, ".hash_tree_algorithm"), SERDE_SKIP)
.field_attribute(c!(CUE_PU, ".hash_tree_salt"), SERDE_SKIP)
.field_attribute(c!(CUE_PU, ".fec_data_extent"), SERDE_SKIP)
.field_attribute(c!(CUE_PU, ".fec_extent"), SERDE_SKIP)
.field_attribute(c!(CUE_PU, ".fec_roots"), SERDE_SKIP)
// Don't serialize fields for incremental OTAs.
.field_attribute(c!(CUE_PU, ".merge_operations"), SERDE_SKIP)
// Don't serialize fields for vendor-signed images, which update_engine
// doesn't support anyway.
.field_attribute(c!(CUE_PU, ".new_partition_signature"), SERDE_SKIP)
// Don't serialize empty lists.
.field_attribute(c!(CUE_DAM, ".apex_info"), SERDE_SKIP_IF_VEC_EMPTY)
.field_attribute(c!(CUE_DAM, ".partitions"), SERDE_SKIP_IF_VEC_EMPTY)
.field_attribute(c!(CUE_DPG, ".partition_names"), SERDE_SKIP_IF_VEC_EMPTY)
.field_attribute(c!(CUE_DPM, ".groups"), SERDE_SKIP_IF_VEC_EMPTY)
.compile_fds(file_descriptors)
.unwrap();
}
+124 -5
View File
@@ -1,14 +1,21 @@
/*
* SPDX-FileCopyrightText: 2023 Andrew Gunnerson
* SPDX-FileCopyrightText: 2023-2024 Andrew Gunnerson
* SPDX-License-Identifier: GPL-3.0-only
*/
use std::sync::atomic::AtomicBool;
use std::{
fmt,
io::{self, IsTerminal},
sync::atomic::{AtomicBool, Ordering},
time::Instant,
};
use anyhow::Result;
use clap::{Parser, Subcommand};
use clap::{Parser, Subcommand, ValueEnum};
use tracing::{debug, Level};
use tracing_subscriber::fmt::{format::Writer, time::FormatTime};
use crate::cli::{avb, boot, completion, cpio, fec, key, ota};
use crate::cli::{avb, boot, completion, cpio, fec, hashtree, key, ota, payload};
#[allow(clippy::large_enum_variant)]
#[derive(Debug, Subcommand)]
@@ -18,8 +25,10 @@ pub enum Command {
Completion(completion::CompletionCli),
Cpio(cpio::CpioCli),
Fec(fec::FecCli),
HashTree(hashtree::HashTreeCli),
Key(key::KeyCli),
Ota(ota::OtaCli),
Payload(payload::PayloadCli),
/// (Deprecated: Use `avbroot ota patch` instead.)
Patch(ota::PatchCli),
/// (Deprecated: Use `avbroot ota extract` instead.)
@@ -28,24 +37,134 @@ pub enum Command {
MagiskInfo(boot::MagiskInfoCli),
}
#[derive(Debug, Clone, Copy, ValueEnum)]
pub enum LogLevel {
Trace,
Debug,
Info,
Warn,
Error,
}
impl LogLevel {
fn as_level(self) -> Level {
match self {
Self::Trace => Level::TRACE,
Self::Debug => Level::DEBUG,
Self::Info => Level::INFO,
Self::Warn => Level::WARN,
Self::Error => Level::ERROR,
}
}
}
impl Default for LogLevel {
fn default() -> Self {
Self::Info
}
}
impl fmt::Display for LogLevel {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.to_possible_value().ok_or(fmt::Error)?.get_name())
}
}
#[derive(Debug, Clone, Copy, ValueEnum)]
pub enum LogFormat {
Short,
Medium,
Long,
}
impl Default for LogFormat {
fn default() -> Self {
Self::Short
}
}
impl fmt::Display for LogFormat {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.to_possible_value().ok_or(fmt::Error)?.get_name())
}
}
#[derive(Debug, Parser)]
#[command(version)]
pub struct Cli {
#[command(subcommand)]
pub command: Command,
/// Lowest log message severity to output.
#[arg(long, global = true, value_name = "LEVEL", default_value_t)]
pub log_level: LogLevel,
/// Output format for log messages.
#[arg(long, global = true, value_name = "FORMAT", default_value_t)]
pub log_format: LogFormat,
}
pub fn main(cancel_signal: &AtomicBool) -> Result<()> {
#[derive(Debug, Clone, Copy)]
pub struct ShortUptime {
epoch: Instant,
}
impl Default for ShortUptime {
fn default() -> Self {
Self {
epoch: Instant::now(),
}
}
}
impl FormatTime for ShortUptime {
fn format_time(&self, w: &mut Writer<'_>) -> fmt::Result {
let e = self.epoch.elapsed();
write!(w, "{:3}.{:03}s", e.as_secs(), e.subsec_millis())
}
}
pub fn init_logging(log_level: LogLevel, log_format: LogFormat) {
let builder = tracing_subscriber::fmt()
.with_writer(io::stderr)
.with_ansi(io::stderr().is_terminal())
.with_max_level(log_level.as_level());
match log_format {
LogFormat::Short => {
let format = tracing_subscriber::fmt::format()
.with_timer(ShortUptime::default())
.with_target(false);
builder.event_format(format).init();
}
LogFormat::Medium => {
builder.with_timer(ShortUptime::default()).init();
}
LogFormat::Long => {
builder.pretty().init();
}
}
}
pub fn main(logging_initialized: &AtomicBool, cancel_signal: &AtomicBool) -> Result<()> {
let cli = Cli::parse();
init_logging(cli.log_level, cli.log_format);
logging_initialized.store(true, Ordering::SeqCst);
debug!(?cli);
match cli.command {
Command::Avb(c) => avb::avb_main(&c, cancel_signal),
Command::Boot(c) => boot::boot_main(&c),
Command::Completion(c) => completion::completion_main(&c),
Command::Cpio(c) => cpio::cpio_main(&c, cancel_signal),
Command::Fec(c) => fec::fec_main(&c, cancel_signal),
Command::HashTree(c) => hashtree::hash_tree_main(&c, cancel_signal),
Command::Key(c) => key::key_main(&c),
Command::Ota(c) => ota::ota_main(&c, cancel_signal),
Command::Payload(c) => payload::payload_main(&c, cancel_signal),
// Deprecated aliases.
Command::Patch(c) => ota::patch_subcommand(&c, cancel_signal),
Command::Extract(c) => ota::extract_subcommand(&c, cancel_signal),
+234 -44
View File
@@ -1,5 +1,5 @@
/*
* SPDX-FileCopyrightText: 2023 Andrew Gunnerson
* SPDX-FileCopyrightText: 2023-2024 Andrew Gunnerson
* SPDX-License-Identifier: GPL-3.0-only
*/
@@ -22,13 +22,13 @@ use clap::{Args, Parser, Subcommand};
use rayon::prelude::{IntoParallelRefIterator, ParallelIterator};
use rsa::RsaPublicKey;
use serde::{Deserialize, Serialize};
use tracing::{debug_span, info, warn, Span};
use crate::{
cli::{status, warning},
crypto::{self, PassphraseSource},
crypto::{self, PassphraseSource, RsaSigningKey},
format::avb::{
self, AlgorithmType, AppendedDescriptorMut, AppendedDescriptorRef, Descriptor, Footer,
Header,
HashTreeDescriptor, Header, KernelCmdlineDescriptor,
},
stream::{self, PSeekFile, Reopen},
util,
@@ -57,16 +57,22 @@ fn read_avb_image(path: &Path) -> Result<(AvbInfo, BufReader<File>)> {
Ok((info, reader))
}
fn write_avb_image(file: PSeekFile, info: &mut AvbInfo) -> Result<()> {
fn write_avb_image(file: PSeekFile, info: &mut AvbInfo, recompute_size: bool) -> Result<()> {
let mut writer = BufWriter::new(file);
if let Some(f) = &mut info.footer {
avb::write_appended_image(&mut writer, &info.header, f, info.image_size)
.context("Failed to write appended AVB image")?;
info.image_size = if let Some(f) = &mut info.footer {
let image_size = if recompute_size {
None
} else {
Some(info.image_size)
};
avb::write_appended_image(&mut writer, &info.header, f, image_size)
.context("Failed to write appended AVB image")?
} else {
avb::write_root_image(&mut writer, &info.header, 4096)
.context("Failed to write root AVB image")?;
}
.context("Failed to write root AVB image")?
};
writer.flush().context("Failed to flush writes")?;
@@ -92,6 +98,18 @@ fn write_info(path: &Path, info: &AvbInfo) -> Result<()> {
Ok(())
}
/// Packing with insecure algorithms is intentionally not supported, so promote
/// to a secure algorithm if needed.
fn promote_insecure_hash_algorithm(algorithm: &mut String) {
const INSECURE_ALGORITHMS: &[&str] = &["sha1"];
const NEW_ALGORITHM: &str = "sha256";
if INSECURE_ALGORITHMS.contains(&algorithm.as_str()) {
warn!("Changing insecure hash algorithm {algorithm} to {NEW_ALGORITHM}");
NEW_ALGORITHM.clone_into(algorithm);
}
}
/// Copy `size` bytes from `reader` into a new file `path` that's opened as
/// both readable and writable.
fn write_raw(
@@ -152,7 +170,7 @@ fn write_raw_and_verify(
if let Err(e) = result {
if ignore_invalid {
warning!("{e:?}");
warn!("{e:?}");
} else {
return Err(e);
}
@@ -179,11 +197,13 @@ fn write_raw_and_update(
match info.header.appended_descriptor_mut()? {
AppendedDescriptorMut::HashTree(d) => {
promote_insecure_hash_algorithm(&mut d.hash_algorithm);
d.image_size = image_size;
d.update(&raw_file, &raw_file, cancel_signal)
d.update(&raw_file, &raw_file, None, cancel_signal)
.context("Failed to update hash tree descriptor")?;
}
AppendedDescriptorMut::Hash(d) => {
promote_insecure_hash_algorithm(&mut d.hash_algorithm);
d.image_size = image_size;
raw_file.rewind()?;
d.update(&mut raw_file, cancel_signal)
@@ -194,6 +214,121 @@ fn write_raw_and_update(
Ok(raw_file)
}
/// Compute the kernel command line arguments to allow the kernel to set up the
/// dm-verity block device without dm-init or userspace helpers. This is handled
/// by init/do_mounts_dm.c in older Pixel devices (and ChromiumOS).
fn compute_dm_verity_cmdline(descriptor: &HashTreeDescriptor) -> String {
use std::fmt::Write;
let mut result = String::new();
// Number of device mapper devices.
result.push_str("dm=\"1");
// Block device name.
result.push_str(" vroot");
// Block device UUID.
result.push_str(" none");
// Block device write mode.
result.push_str(" ro");
// Number of device mapper targets.
result.push_str(" 1,");
// Starting sector.
result.push('0');
// Sector count.
write!(&mut result, " {}", descriptor.image_size / 512).unwrap();
// dm-verity version.
write!(&mut result, " verity {}", descriptor.dm_verity_version).unwrap();
// Data block device (replaced by bootloader at runtime).
result.push_str(" PARTUUID=$(ANDROID_SYSTEM_PARTUUID)");
// Hash block device (replaced by bootloader at runtime).
result.push_str(" PARTUUID=$(ANDROID_SYSTEM_PARTUUID)");
// Data block size.
write!(&mut result, " {}", descriptor.data_block_size).unwrap();
// Hash block size.
write!(&mut result, " {}", descriptor.hash_block_size).unwrap();
// Number of data blocks.
write!(
&mut result,
" {}",
descriptor.image_size / u64::from(descriptor.data_block_size),
)
.unwrap();
// Hash starting block (in units of the hash block size).
write!(
&mut result,
" {}",
descriptor.image_size / u64::from(descriptor.hash_block_size),
)
.unwrap();
// Hash algorithm.
write!(&mut result, " {}", descriptor.hash_algorithm).unwrap();
// Root digest.
write!(&mut result, " {}", &hex::encode(&descriptor.root_digest)).unwrap();
// Salt.
write!(&mut result, " {}", &hex::encode(&descriptor.salt)).unwrap();
// Number of optional arguments.
let num_optional_args = if descriptor.fec_num_roots != 0 { 10 } else { 2 }
+ u8::from(descriptor.flags & HashTreeDescriptor::FLAG_CHECK_AT_MOST_ONCE != 0);
write!(&mut result, " {num_optional_args}").unwrap();
if descriptor.flags & HashTreeDescriptor::FLAG_CHECK_AT_MOST_ONCE != 0 {
// [n + 1] Only check blocks once instead of on each access.
result.push_str(" check_at_most_once");
}
// [0] Corruption handling mode (replaced by bootloader at runtime).
result.push_str(" $(ANDROID_VERITY_MODE)");
// [1] Force return zeros and skip validation for blocks expected to contain
// only zeros.
result.push_str(" ignore_zero_blocks");
if descriptor.fec_num_roots != 0 {
// [2-3] Enable FEC (replaced by bootloader at runtime).
result.push_str(" use_fec_from_device PARTUUID=$(ANDROID_SYSTEM_PARTUUID)");
// [4-5] Number of parity bytes per FEC codeword.
write!(&mut result, " fec_roots {}", descriptor.fec_num_roots).unwrap();
let fec_block_offset = descriptor.fec_offset / u64::from(descriptor.data_block_size);
// [6-7] Number of data blocks covered by FEC.
write!(&mut result, " fec_blocks {fec_block_offset}").unwrap();
// [8-9] Starting block (in data block size units) of FEC.
write!(&mut result, " fec_start {fec_block_offset}").unwrap();
}
// Root filesystem block device.
result.push_str("\" root=/dev/dm-0");
result
}
/// Update the dm-verity kernel command line descriptor to match the hash tree
/// descriptor. This is a no-op if there's no matching existing kernel command
/// line descriptor to update. Returns whether the descriptor was updated.
fn update_dm_verity_cmdline(info: &mut AvbInfo) -> Result<bool> {
assert!(info.footer.is_some(), "Not an appended image");
let new_cmdline = match info.header.appended_descriptor()? {
AppendedDescriptorRef::HashTree(d) => compute_dm_verity_cmdline(d),
AppendedDescriptorRef::Hash(_) => return Ok(false),
};
for d in &mut info.header.descriptors {
if let Descriptor::KernelCmdline(d) = d {
if d.flags & KernelCmdlineDescriptor::FLAG_USE_ONLY_IF_HASHTREE_NOT_DISABLED != 0
&& d.cmdline.starts_with("dm=")
&& d.cmdline != new_cmdline
{
d.cmdline = new_cmdline;
return Ok(true);
}
}
}
Ok(false)
}
/// Sign or clear header signatures based on whether the original header was
/// signed. If the original header was signed and is unchanged, then the
/// original signature is used as-is. If the force option is specified, then
@@ -227,16 +362,16 @@ fn sign_or_clear(info: &mut AvbInfo, orig_header: &Header, key_group: &KeyGroup)
match sign_action {
SignAction::None => {
if originally_signed {
status!("Preserving original AVB header signature");
info!("Preserving original AVB header signature");
} else {
status!("Leaving AVB header unsigned");
info!("Leaving AVB header unsigned");
}
}
SignAction::Sign => {
if originally_signed {
status!("Replacing AVB header signature");
info!("Replacing AVB header signature");
} else {
status!("Signing AVB header");
info!("Signing AVB header");
}
let Some(key_path) = &key_group.key else {
@@ -248,19 +383,33 @@ fn sign_or_clear(info: &mut AvbInfo, orig_header: &Header, key_group: &KeyGroup)
key_group.pass_file.as_deref(),
key_group.pass_env_var.as_deref(),
);
let private_key = crypto::read_pem_key_file(key_path, &source)
.with_context(|| format!("Failed to load key: {key_path:?}"))?;
let signing_key = if let Some(helper) = &key_group.signing_helper {
let public_key = crypto::read_pem_public_key_file(key_path)
.with_context(|| format!("Failed to load key: {key_path:?}"))?;
info.header.set_algo_for_key(&private_key)?;
RsaSigningKey::External {
program: helper.clone(),
public_key_file: key_path.clone(),
public_key,
passphrase_source: source,
}
} else {
let private_key = crypto::read_pem_key_file(key_path, &source)
.with_context(|| format!("Failed to load key: {key_path:?}"))?;
RsaSigningKey::Internal(private_key)
};
info.header.set_algo_for_key(&signing_key)?;
info.header
.sign(&private_key)
.sign(&signing_key)
.context("Failed to sign new AVB header")?;
}
SignAction::Clear => {
if originally_signed {
status!("Clearing AVB header signature");
info!("Clearing AVB header signature");
} else {
status!("Leaving AVB header unsigned");
info!("Leaving AVB header unsigned");
}
info.header.algorithm_type = AlgorithmType::None;
@@ -320,15 +469,15 @@ pub fn verify_headers(
if let Some(e) = expected_key {
if k == e {
status!("{prefix}");
info!("{prefix}");
} else {
bail!("{prefix}, but is signed by an untrusted key");
}
} else {
warning!("{prefix}, but parent does not list a trusted key");
warn!("{prefix}, but parent does not list a trusted key");
}
} else {
status!("{name} has an unsigned vbmeta header");
info!("{name} has an unsigned vbmeta header");
}
for descriptor in &header.descriptors {
@@ -371,6 +520,7 @@ fn verify_and_repair(
repair: bool,
cancel_signal: &AtomicBool,
) -> Result<()> {
let _span = debug_span!("image", name = name.unwrap_or_default()).entered();
let suffix = match name {
Some(n) => format!(" for: {n}"),
None => String::new(),
@@ -378,21 +528,18 @@ fn verify_and_repair(
match descriptor {
AppendedDescriptorRef::HashTree(d) => {
status!("Verifying hash tree descriptor{suffix}");
info!("Verifying hash tree descriptor{suffix}");
match d.verify(&file, cancel_signal) {
Err(
e @ avb::Error::InvalidRootDigest { .. }
| e @ avb::Error::InvalidHashTree { .. },
) if repair => {
warning!("Failed to verify hash tree descriptor{suffix}: {e}");
warning!("Attempting to repair using FEC data{suffix}");
Err(e @ avb::Error::HashTree(_)) if repair => {
warn!("Failed to verify hash tree descriptor{suffix}: {e}");
warn!("Attempting to repair using FEC data{suffix}");
d.repair(&file, &file, cancel_signal)
.with_context(|| format!("Failed to repair data{suffix}"))?;
d.verify(&file, cancel_signal).map(|_| {
status!("Successfully repaired data{suffix}");
info!("Successfully repaired data{suffix}");
})
}
ret => ret,
@@ -400,7 +547,7 @@ fn verify_and_repair(
.with_context(|| format!("Failed to verify hash tree descriptor{suffix}"))?;
}
AppendedDescriptorRef::Hash(d) => {
status!("Verifying hash descriptor{suffix}");
info!("Verifying hash descriptor{suffix}");
file.rewind()?;
d.verify(file, cancel_signal)
@@ -419,9 +566,13 @@ pub fn verify_descriptors(
repair: bool,
cancel_signal: &AtomicBool,
) -> Result<()> {
let parent_span = Span::current();
descriptors
.par_iter()
.map(|(name, descriptor)| {
let _span = parent_span.enter();
let path = format!("{name}.img");
let file = match directory
.open_with(&path, OpenOptions::new().read(true).write(repair))
@@ -432,7 +583,7 @@ pub fn verify_descriptors(
// refer to partitions that exist on the device, but not in the
// OTA.
Err(e) if e.kind() == io::ErrorKind::NotFound => {
warning!("Partition image does not exist: {path:?}");
warn!("Partition image does not exist: {path:?}");
return Ok(());
}
Err(e) => {
@@ -481,7 +632,11 @@ fn pack_subcommand(cli: &PackCli, cancel_signal: &AtomicBool) -> Result<()> {
format!("Failed to open raw image for reading: {:?}", cli.input_raw)
})?;
write_raw_and_update(&cli.output, &mut reader, &mut info, cancel_signal)?
let file = write_raw_and_update(&cli.output, &mut reader, &mut info, cancel_signal)?;
update_dm_verity_cmdline(&mut info)?;
file
} else {
File::create(&cli.output)
.map(PSeekFile::new)
@@ -490,12 +645,16 @@ fn pack_subcommand(cli: &PackCli, cancel_signal: &AtomicBool) -> Result<()> {
sign_or_clear(&mut info, &orig_header, &cli.key)?;
write_avb_image(file, &mut info)?;
write_avb_image(file, &mut info, cli.recompute_size)?;
// We display the info at the very end after both the header and footer are
// updated so that incorrect/incomplete information isn't shown.
display_info(&cli.display, &info);
if let Some(path) = &cli.output_info {
write_info(path, &info)?;
}
Ok(())
}
@@ -507,11 +666,14 @@ fn repack_subcommand(cli: &RepackCli, cancel_signal: &AtomicBool) -> Result<()>
let file = write_raw_and_verify(&cli.output, &mut reader, &info, false, cancel_signal)?;
// Write new hash tree and FEC data instead of copying the original.
// THere could have been errors in the original FEC data itself.
// There could have been errors in the original FEC data itself.
if let AppendedDescriptorMut::HashTree(d) = info.header.appended_descriptor_mut()? {
d.update(&file, &file, cancel_signal)?;
promote_insecure_hash_algorithm(&mut d.hash_algorithm);
d.update(&file, &file, None, cancel_signal)?;
}
update_dm_verity_cmdline(&mut info)?;
file
} else {
File::create(&cli.output)
@@ -521,7 +683,7 @@ fn repack_subcommand(cli: &RepackCli, cancel_signal: &AtomicBool) -> Result<()>
sign_or_clear(&mut info, &orig_header, &cli.key)?;
write_avb_image(file, &mut info)?;
write_avb_image(file, &mut info, false)?;
// We display the info at the very end after both the header and footer are
// updated so that incorrect/incomplete information isn't shown.
@@ -571,7 +733,7 @@ fn verify_subcommand(cli: &VerifyCli, cancel_signal: &AtomicBool) -> Result<()>
)?;
verify_descriptors(&directory, &descriptors, cli.repair, cancel_signal)?;
status!("Successfully verified all vbmeta signatures and hashes");
info!("Successfully verified all vbmeta signatures and hashes");
Ok(())
}
@@ -595,12 +757,15 @@ struct DisplayGroup {
#[derive(Debug, Args)]
struct KeyGroup {
/// Path to private key for signing.
/// Path to signing key.
///
/// A private key is needed if packing an image where the original header
/// A signing key is needed if packing an image where the original header
/// was signed and the header needs to be modified (eg. for a new checksum).
/// If the header was originally not signed, then the private key is not
/// If the header was originally not signed, then the signing key is not
/// used, unless --force is specified.
///
/// This should normally be a private key. However, if --signing-helper is
/// used, then it should be a public key instead.
#[arg(short, long, value_name = "FILE", value_parser)]
key: Option<PathBuf>,
@@ -620,6 +785,15 @@ struct KeyGroup {
/// File containing private key passphrase.
#[arg(long, value_name = "FILE", value_parser, group = "pass")]
pass_file: Option<PathBuf>,
/// External program for signing.
///
/// If this option is specified, then --key must refer to a public key. The
/// program will be invoked as:
///
/// <program> <algo> <public key> [file <pass file>|env <pass env>]
#[arg(long, value_name = "PROGRAM", value_parser)]
signing_helper: Option<PathBuf>,
}
/// Unpack an AVB image.
@@ -689,12 +863,28 @@ struct PackCli {
#[arg(long, value_name = "FILE", value_parser, default_value = "avb.toml")]
input_info: PathBuf,
/// Path to output AVB info TOML.
///
/// If specified, the AVB info containing all recomputed fields will be
/// written to this file. This can point to the same file as --input-info.
#[arg(long, value_name = "FILE", value_parser)]
output_info: Option<PathBuf>,
/// Path to input raw image.
///
/// Appended AVB images require a raw image.
#[arg(long, value_name = "FILE", value_parser, default_value = "raw.img")]
input_raw: PathBuf,
/// Recompute image size.
///
/// By default, it is assumed that the image has a fixed size specified by
/// the image_size top-level field in the AVB info TOML. If flag is passed,
/// then that field is ignored and the smallest possible output image will
/// be created.
#[arg(long)]
recompute_size: bool,
#[command(flatten)]
key: KeyGroup,
+1 -1
View File
@@ -303,7 +303,7 @@ struct UnpackCli {
#[arg(short, long, value_name = "FILE", value_parser)]
input: PathBuf,
/// Path to output cpio info TOML.
/// Path to output info TOML.
#[arg(long, value_name = "FILE", value_parser, default_value = "cpio.toml")]
output_info: PathBuf,
+40
View File
@@ -61,6 +61,24 @@ fn generate_subcommand(cli: &GenerateCli, cancel_signal: &AtomicBool) -> Result<
Ok(())
}
fn update_subcommand(cli: &UpdateCli, cancel_signal: &AtomicBool) -> Result<()> {
let ranges = cli
.range
.chunks_exact(2)
.map(|w| w[0]..w[1])
.collect::<Vec<_>>();
let input = open_input(&cli.input, false)?;
let mut fec = read_fec(&cli.fec)?;
fec.update(&input, &ranges, cancel_signal)
.context("Failed to update FEC data")?;
write_fec(&cli.fec, &fec)?;
Ok(())
}
fn verify_subcommand(cli: &VerifyCli, cancel_signal: &AtomicBool) -> Result<()> {
let input = open_input(&cli.input, false)?;
let fec = read_fec(&cli.fec)?;
@@ -87,6 +105,7 @@ fn repair_subcommand(cli: &RepairCli, cancel_signal: &AtomicBool) -> Result<()>
pub fn fec_main(cli: &FecCli, cancel_signal: &AtomicBool) -> Result<()> {
match &cli.command {
FecCommand::Generate(c) => generate_subcommand(c, cancel_signal),
FecCommand::Update(c) => update_subcommand(c, cancel_signal),
FecCommand::Verify(c) => verify_subcommand(c, cancel_signal),
FecCommand::Repair(c) => repair_subcommand(c, cancel_signal),
}
@@ -108,6 +127,26 @@ struct GenerateCli {
parity: u8,
}
/// Update FEC data after a file is modified.
#[derive(Debug, Parser)]
struct UpdateCli {
/// Path to input data.
#[arg(short, long, value_name = "FILE", value_parser)]
input: PathBuf,
/// Path to FEC data.
///
/// The file will be modified in place.
#[arg(short, long, value_name = "FILE", value_parser)]
fec: PathBuf,
/// Input file ranges that were updated.
///
/// This is a half-open range and can be specified multiple times.
#[arg(short, long, value_names = ["START", "END"], num_args = 2)]
range: Vec<u64>,
}
/// Verify that a file contains no errors.
#[derive(Debug, Parser)]
struct VerifyCli {
@@ -137,6 +176,7 @@ struct RepairCli {
#[derive(Debug, Subcommand)]
enum FecCommand {
Generate(GenerateCli),
Update(UpdateCli),
Verify(VerifyCli),
Repair(RepairCli),
}
+176
View File
@@ -0,0 +1,176 @@
/*
* SPDX-FileCopyrightText: 2023 Andrew Gunnerson
* SPDX-License-Identifier: GPL-3.0-only
*/
use std::{
fs::{File, OpenOptions},
io::{BufReader, BufWriter, Write},
path::{Path, PathBuf},
sync::atomic::AtomicBool,
};
use anyhow::{Context, Result};
use clap::{Parser, Subcommand};
use crate::{
format::hashtree::HashTreeImage,
stream::{FromReader, PSeekFile, ToWriter},
};
fn open_input(path: &Path, rw: bool) -> Result<PSeekFile> {
OpenOptions::new()
.read(true)
.write(rw)
.open(path)
.map(PSeekFile::new)
.with_context(|| format!("Failed to open file: {path:?}"))
}
fn read_hash_tree(path: &Path) -> Result<HashTreeImage> {
let reader = File::open(path)
.map(BufReader::new)
.with_context(|| format!("Failed to open for reading: {path:?}"))?;
let hash_tree = HashTreeImage::from_reader(reader)
.with_context(|| format!("Failed to read hash tree data: {path:?}"))?;
Ok(hash_tree)
}
fn write_hash_tree(path: &Path, hash_tree: &HashTreeImage) -> Result<()> {
let mut writer = File::create(path)
.map(BufWriter::new)
.with_context(|| format!("Failed to open for writing: {path:?}"))?;
hash_tree
.to_writer(&mut writer)
.with_context(|| format!("Failed to write hash tree data: {path:?}"))?;
writer
.flush()
.with_context(|| format!("Failed to flush hash tree data: {path:?}"))?;
Ok(())
}
fn generate_subcommand(cli: &GenerateCli, cancel_signal: &AtomicBool) -> Result<()> {
let salt = hex::decode(&cli.salt).context("Invalid salt")?;
let input = open_input(&cli.input, false)?;
let hash_tree =
HashTreeImage::generate(&input, cli.block_size, &cli.algorithm, &salt, cancel_signal)
.context("Failed to generate hash tree data")?;
write_hash_tree(&cli.hash_tree, &hash_tree)?;
Ok(())
}
fn update_subcommand(cli: &UpdateCli, cancel_signal: &AtomicBool) -> Result<()> {
let ranges = cli
.range
.chunks_exact(2)
.map(|w| w[0]..w[1])
.collect::<Vec<_>>();
let input = open_input(&cli.input, false)?;
let mut hash_tree = read_hash_tree(&cli.hash_tree)?;
hash_tree
.update(&input, &ranges, cancel_signal)
.context("Failed to update hash tree data")?;
write_hash_tree(&cli.hash_tree, &hash_tree)?;
Ok(())
}
fn verify_subcommand(cli: &VerifyCli, cancel_signal: &AtomicBool) -> Result<()> {
let input = open_input(&cli.input, false)?;
let hash_tree = read_hash_tree(&cli.hash_tree)?;
hash_tree
.verify(&input, cancel_signal)
.context("Failed to verify data")?;
Ok(())
}
pub fn hash_tree_main(cli: &HashTreeCli, cancel_signal: &AtomicBool) -> Result<()> {
match &cli.command {
HashTreeCommand::Generate(c) => generate_subcommand(c, cancel_signal),
HashTreeCommand::Update(c) => update_subcommand(c, cancel_signal),
HashTreeCommand::Verify(c) => verify_subcommand(c, cancel_signal),
}
}
/// Generate hash tree data for a file.
#[derive(Debug, Parser)]
struct GenerateCli {
/// Path to input data.
#[arg(short, long, value_name = "FILE", value_parser)]
input: PathBuf,
/// Path to output hash tree data.
#[arg(short = 'H', long, value_name = "FILE", value_parser)]
hash_tree: PathBuf,
/// Block size.
#[arg(short, long, value_name = "BYTES", default_value = "4096")]
block_size: u32,
/// Hash algorithm.
#[arg(short, long, value_name = "NAME", default_value = "sha256")]
algorithm: String,
/// Salt (in hex).
#[arg(short, long, value_name = "HEX", default_value = "")]
salt: String,
}
/// Update hash tree data after a file is modified.
#[derive(Debug, Parser)]
struct UpdateCli {
/// Path to input data.
#[arg(short, long, value_name = "FILE", value_parser)]
input: PathBuf,
/// Path to hash tree data.
///
/// The file will be modified in place.
#[arg(short = 'H', long, value_name = "FILE", value_parser)]
hash_tree: PathBuf,
/// Input file ranges that were updated.
///
/// This is a half-open range and can be specified multiple times.
#[arg(short, long, value_names = ["START", "END"], num_args = 2)]
range: Vec<u64>,
}
/// Verify that a file contains no errors.
#[derive(Debug, Parser)]
struct VerifyCli {
/// Path to input data.
#[arg(short, long, value_name = "FILE", value_parser)]
input: PathBuf,
/// Path to input hash tree data.
#[arg(short = 'H', long, value_name = "FILE", value_parser)]
hash_tree: PathBuf,
}
#[derive(Debug, Subcommand)]
enum HashTreeCommand {
Generate(GenerateCli),
Update(UpdateCli),
Verify(VerifyCli),
}
/// Generate dm-verity hash tree data and verify files.
///
/// These commands operate on a standard hash tree data prepended by a custom
/// header.
#[derive(Debug, Parser)]
pub struct HashTreeCli {
#[command(subcommand)]
command: HashTreeCommand,
}
+30
View File
@@ -55,6 +55,9 @@ pub fn key_main(cli: &KeyCli) -> Result<()> {
.with_context(|| format!("Failed to load key: {p:?}"))?;
private_key.to_public_key()
} else if let Some(p) = &c.input.public_key {
crypto::read_pem_public_key_file(p)
.with_context(|| format!("Failed to load public key: {p:?}"))?
} else if let Some(p) = &c.input.cert {
let certificate = crypto::read_pem_cert_file(p)
.with_context(|| format!("Failed to load certificate: {p:?}"))?;
@@ -71,6 +74,16 @@ pub fn key_main(cli: &KeyCli) -> Result<()> {
fs::write(&c.output, encoded)
.with_context(|| format!("Failed to write public key: {:?}", c.output))?;
}
KeyCommand::DecodeAvb(c) => {
let encoded = fs::read(&c.key)
.with_context(|| format!("Failed to load AVB public key: {:?}", c.key))?;
let public_key = avb::decode_public_key(&encoded)
.context("Failed to decode public key as AVB format")?;
crypto::write_pem_public_key_file(&c.output, &public_key)
.with_context(|| format!("Failed to write public key: {:?}", c.output))?;
}
}
Ok(())
@@ -83,6 +96,10 @@ struct PublicKeyInputGroup {
#[arg(short, long, value_name = "FILE", value_parser)]
key: Option<PathBuf>,
/// Path to public key.
#[arg(short, long, value_name = "FILE", value_parser, conflicts_with_all = ["pass_env_var", "pass_file"])]
public_key: Option<PathBuf>,
/// Path to certificate.
#[arg(short, long, value_name = "FILE", value_parser, conflicts_with_all = ["pass_env_var", "pass_file"])]
cert: Option<PathBuf>,
@@ -152,11 +169,24 @@ struct ExtractAvbCli {
passphrase: PassphraseGroup,
}
/// Convert an AVB-encoded public key to a PKCS8-encoded public key.
#[derive(Debug, Parser)]
struct DecodeAvbCli {
/// Path to output PKCS8-encoded public key.
#[arg(short, long, value_name = "FILE", value_parser)]
output: PathBuf,
/// Path to AVB-encoded public key.
#[arg(short, long, value_name = "FILE", value_parser)]
key: PathBuf,
}
#[derive(Debug, Subcommand)]
enum KeyCommand {
GenerateKey(GenerateKeyCli),
GenerateCert(GenerateCertCli),
ExtractAvb(ExtractAvbCli),
DecodeAvb(DecodeAvbCli),
}
/// Generate and convert keys.
+3 -16
View File
@@ -1,5 +1,5 @@
/*
* SPDX-FileCopyrightText: 2023 Andrew Gunnerson
* SPDX-FileCopyrightText: 2023-2024 Andrew Gunnerson
* SPDX-License-Identifier: GPL-3.0-only
*/
@@ -9,20 +9,7 @@ pub mod boot;
pub mod completion;
pub mod cpio;
pub mod fec;
pub mod hashtree;
pub mod key;
pub mod ota;
macro_rules! status {
($($arg:tt)*) => {
eprintln!("\x1b[1m[*] {}\x1b[0m", format!($($arg)*))
}
}
macro_rules! warning {
($($arg:tt)*) => {
eprintln!("\x1b[1;31m[WARNING] {}\x1b[0m", format!($($arg)*))
}
}
pub(crate) use status;
pub(crate) use warning;
pub mod payload;
+865 -413
View File
File diff suppressed because it is too large Load Diff
+473
View File
@@ -0,0 +1,473 @@
/*
* SPDX-FileCopyrightText: 2024 Andrew Gunnerson
* SPDX-License-Identifier: GPL-3.0-only
*/
use std::{
collections::HashMap,
ffi::{OsStr, OsString},
fs::{self, File},
io::{BufReader, BufWriter, Seek, SeekFrom},
path::{Path, PathBuf},
sync::atomic::AtomicBool,
};
use anyhow::{anyhow, bail, Context, Result};
use cap_std::{ambient_authority, fs::Dir};
use clap::{Args, Parser, Subcommand};
use tracing::info;
use crate::{
cli::ota,
crypto::{self, PassphraseSource, RsaSigningKey},
format::payload::{PayloadHeader, PayloadWriter},
stream::{self, FromReader, PSeekFile},
};
fn open_reader(path: &Path) -> Result<(BufReader<File>, PayloadHeader)> {
let mut reader = File::open(path)
.map(BufReader::new)
.with_context(|| format!("Failed to open payload for reading: {path:?}"))?;
let header = PayloadHeader::from_reader(&mut reader)
.with_context(|| format!("Failed to read payload header: {path:?}"))?;
if !header.is_full_ota() {
bail!("Payload is a delta OTA, not a full OTA");
}
Ok((reader, header))
}
fn open_writer(
path: &Path,
header: PayloadHeader,
key: RsaSigningKey,
) -> Result<PayloadWriter<BufWriter<File>>> {
let writer = File::create(path)
.map(BufWriter::new)
.with_context(|| format!("Failed to open payload for writing: {path:?}"))?;
let payload_writer = PayloadWriter::new(writer, header, key)
.with_context(|| format!("Failed to write payload header: {path:?}"))?;
Ok(payload_writer)
}
fn read_info(path: &Path) -> Result<PayloadHeader> {
let data = fs::read_to_string(path)
.with_context(|| format!("Failed to read payload info TOML: {path:?}"))?;
let info = toml_edit::de::from_str(&data)
.with_context(|| format!("Failed to parse payload info TOML: {path:?}"))?;
Ok(info)
}
fn write_info(path: &Path, manifest: &PayloadHeader) -> Result<()> {
let data = toml_edit::ser::to_string_pretty(manifest)
.with_context(|| format!("Failed to serialize payload info TOML: {path:?}"))?;
fs::write(path, data)
.with_context(|| format!("Failed to write payload info TOML: {path:?}"))?;
Ok(())
}
fn display_header(cli: &PayloadCli, header: &PayloadHeader) {
if !cli.quiet {
println!("{header:#?}");
}
}
fn load_key(group: &KeyGroup) -> Result<RsaSigningKey> {
let source = PassphraseSource::new(
&group.key,
group.pass_file.as_deref(),
group.pass_env_var.as_deref(),
);
let signing_key = if let Some(helper) = &group.signing_helper {
let public_key = crypto::read_pem_public_key_file(&group.key)
.with_context(|| format!("Failed to load key: {:?}", group.key))?;
RsaSigningKey::External {
program: helper.clone(),
public_key_file: group.key.clone(),
public_key,
passphrase_source: source,
}
} else {
let private_key = crypto::read_pem_key_file(&group.key, &source)
.with_context(|| format!("Failed to load key: {:?}", group.key))?;
RsaSigningKey::Internal(private_key)
};
Ok(signing_key)
}
fn unpack_subcommand(
payload_cli: &PayloadCli,
cli: &UnpackCli,
cancel_signal: &AtomicBool,
) -> Result<()> {
let (mut reader, header) = open_reader(&cli.input)?;
let payload_size = reader
.seek(SeekFrom::End(0))
.with_context(|| format!("Failed to get file size: {:?}", cli.input))?;
display_header(payload_cli, &header);
write_info(&cli.output_info, &header)?;
let authority = ambient_authority();
Dir::create_ambient_dir_all(&cli.output_images, authority)
.with_context(|| format!("Failed to create directory: {:?}", cli.output_images))?;
let directory = Dir::open_ambient_dir(&cli.output_images, authority)
.with_context(|| format!("Failed to open directory: {:?}", cli.output_images))?;
ota::extract_payload(
&PSeekFile::new(reader.into_inner()),
&directory,
0,
payload_size,
&header,
&header
.manifest
.partitions
.iter()
.map(|p| &p.partition_name)
.cloned()
.collect(),
cancel_signal,
)?;
Ok(())
}
fn pack_subcommand(
payload_cli: &PayloadCli,
cli: &PackCli,
cancel_signal: &AtomicBool,
) -> Result<()> {
let signing_key = load_key(&cli.key)?;
let mut header = read_info(&cli.input_info)?;
let authority = ambient_authority();
let directory = Dir::open_ambient_dir(&cli.input_images, authority)
.with_context(|| format!("Failed to open directory: {:?}", cli.input_images))?;
for p in &header.manifest.partitions {
let name = &p.partition_name;
if Path::new(name).file_name() != Some(OsStr::new(name)) {
bail!("Unsafe partition name: {name}");
}
}
// Pre-open all of the image files.
let input_files = header
.manifest
.partitions
.iter()
.map(|p| {
let path = format!("{}.img", p.partition_name);
let file = directory
.open(&path)
.map(|f| PSeekFile::new(f.into_std()))
.with_context(|| format!("Failed to open file: {path:?}"))?;
Ok((p.partition_name.clone(), file))
})
.collect::<Result<HashMap<_, _>>>()?;
// Compress the images and compute the list of install operations for
// insertion into the payload header. The compressed data is stored in new
// temp files and the original input files are dropped.
let mut compressed_files = input_files
.into_iter()
.map(|(name, mut input_file)| {
ota::compress_image(&name, &mut input_file, &mut header, None, cancel_signal)
.with_context(|| format!("Failed to compress image: {name}"))?;
Ok((name, input_file))
})
.collect::<Result<HashMap<_, _>>>()?;
info!("Generating new OTA payload");
// Now we can write the actual payload. With everything precomputed, this is
// mostly just a simple copy.
let mut payload_writer = open_writer(&cli.output, header.clone(), signing_key)?;
while payload_writer
.begin_next_operation()
.context("Failed to begin next payload blob entry")?
{
let name = payload_writer.partition().unwrap().partition_name.clone();
let operation = payload_writer.operation().unwrap();
let Some(data_length) = operation.data_length else {
// Otherwise, this is a ZERO/DISCARD operation.
continue;
};
let pi = payload_writer.partition_index().unwrap();
let oi = payload_writer.operation_index().unwrap();
let orig_partition = &header.manifest.partitions[pi];
let orig_operation = &orig_partition.operations[oi];
let data_offset = orig_operation
.data_offset
.ok_or_else(|| anyhow!("Missing data_offset in partition #{pi} operation #{oi}"))?;
// The compressed chunks are laid out sequentially and data_offset is
// set to the offset within that file.
let Some(input_file) = compressed_files.get_mut(&name) else {
unreachable!("Compressed data not found for image: {name}");
};
input_file
.seek(SeekFrom::Start(data_offset))
.with_context(|| format!("Failed to seek image: {name}"))?;
stream::copy_n(input_file, &mut payload_writer, data_length, cancel_signal)
.with_context(|| format!("Failed to copy from replacement image: {name}"))?;
}
let (_, header, properties, _) = payload_writer
.finish()
.context("Failed to finalize payload")?;
// Display the header information now that it has been finalized.
display_header(payload_cli, &header);
// Optionally, write payload_properties.txt.
if let Some(path) = &cli.output_properties {
fs::write(path, properties)
.with_context(|| format!("Failed to write payload properties: {path:?}"))?;
}
Ok(())
}
fn repack_subcommand(
payload_cli: &PayloadCli,
cli: &RepackCli,
cancel_signal: &AtomicBool,
) -> Result<()> {
let signing_key = load_key(&cli.key)?;
let (mut reader, header) = open_reader(&cli.input)?;
info!("Generating new OTA payload");
let mut payload_writer = open_writer(&cli.output, header.clone(), signing_key)?;
while payload_writer
.begin_next_operation()
.context("Failed to begin next payload blob entry")?
{
let name = payload_writer.partition().unwrap().partition_name.clone();
let operation = payload_writer.operation().unwrap();
let Some(data_length) = operation.data_length else {
// Otherwise, this is a ZERO/DISCARD operation.
continue;
};
let pi = payload_writer.partition_index().unwrap();
let oi = payload_writer.operation_index().unwrap();
let orig_partition = &header.manifest.partitions[pi];
let orig_operation = &orig_partition.operations[oi];
let data_offset = orig_operation
.data_offset
.ok_or_else(|| anyhow!("Missing data_offset in partition #{pi} operation #{oi}"))?;
// Directly copy blobs from the original payload.
let data_offset = data_offset
.checked_add(header.blob_offset)
.ok_or_else(|| anyhow!("data_offset overflow in partition #{pi} operation #{oi}"))?;
reader
.seek(SeekFrom::Start(data_offset))
.with_context(|| format!("Failed to seek original payload to {data_offset}"))?;
stream::copy_n(&mut reader, &mut payload_writer, data_length, cancel_signal)
.with_context(|| format!("Failed to copy from original payload: {name}"))?;
}
let (_, header, properties, _) = payload_writer
.finish()
.context("Failed to finalize payload")?;
// Display the header information now that it has been finalized.
display_header(payload_cli, &header);
// Optionally, write payload_properties.txt.
if let Some(path) = &cli.output_properties {
fs::write(path, properties)
.with_context(|| format!("Failed to write payload properties: {path:?}"))?;
}
Ok(())
}
fn info_subcommand(payload_cli: &PayloadCli, cli: &InfoCli) -> Result<()> {
let (_, header) = open_reader(&cli.input)?;
display_header(payload_cli, &header);
Ok(())
}
pub fn payload_main(cli: &PayloadCli, cancel_signal: &AtomicBool) -> Result<()> {
match &cli.command {
PayloadCommand::Unpack(c) => unpack_subcommand(cli, c, cancel_signal),
PayloadCommand::Pack(c) => pack_subcommand(cli, c, cancel_signal),
PayloadCommand::Repack(c) => repack_subcommand(cli, c, cancel_signal),
PayloadCommand::Info(c) => info_subcommand(cli, c),
}
}
#[derive(Debug, Args)]
struct KeyGroup {
/// Path to signing key.
///
/// This should normally be a private key. However, if --signing-helper is
/// used, then it should be a public key instead.
#[arg(short, long, value_name = "FILE", value_parser)]
key: PathBuf,
/// Environment variable containing private key passphrase.
#[arg(long, value_name = "ENV_VAR", value_parser, group = "pass")]
pass_env_var: Option<OsString>,
/// File containing private key passphrase.
#[arg(long, value_name = "FILE", value_parser, group = "pass")]
pass_file: Option<PathBuf>,
/// External program for signing.
///
/// If this option is specified, then --key must refer to a public key. The
/// program will be invoked as:
///
/// <program> <algo> <public key> [file <pass file>|env <pass env>]
#[arg(long, value_name = "PROGRAM", value_parser)]
signing_helper: Option<PathBuf>,
}
/// Unpack a payload binary.
///
/// Each partition is extracted to `<partition name>.img` in the output images
/// directory. The payload header metadata is written to the info TOML file.
///
/// If any partition names are unsafe to use in a path, the extraction process
/// will fail and exit. Extracted files are never written outside of the tree
/// directory, even if an external process tries to interfere.
#[derive(Debug, Parser)]
struct UnpackCli {
/// Path to input payload binary.
#[arg(short, long, value_name = "FILE", value_parser)]
input: PathBuf,
/// Path to output info TOML.
#[arg(
long,
value_name = "FILE",
value_parser,
default_value = "payload.toml"
)]
output_info: PathBuf,
/// Path to output images directory.
#[arg(
long,
value_name = "DIR",
value_parser,
default_value = "payload_images"
)]
output_images: PathBuf,
}
/// Pack a payload binary.
///
/// The new payload binary will *only* contain images listed in the info TOML
/// file. Extra images in the input images directory that aren't listed will be
/// silently ignored. Images are added to the payload in the order that they are
/// listed in the info TOML file.
#[derive(Debug, Parser)]
struct PackCli {
/// Path to output payload binary.
#[arg(short, long, value_name = "FILE", value_parser)]
output: PathBuf,
/// Path to output payload properties file.
#[arg(short, long, value_name = "FILE", value_parser)]
output_properties: Option<PathBuf>,
/// Path to input info TOML.
#[arg(
long,
value_name = "FILE",
value_parser,
default_value = "payload.toml"
)]
input_info: PathBuf,
/// Path to input images directory.
#[arg(
long,
value_name = "DIR",
value_parser,
default_value = "payload_images"
)]
input_images: PathBuf,
#[command(flatten)]
key: KeyGroup,
}
/// Repack a payload binary.
///
/// This command is equivalent to running `unpack` and `pack`, except without
/// storing the unpacked data to disk nor recompressing the partition images.
#[derive(Debug, Parser)]
struct RepackCli {
/// Path to input payload binary.
#[arg(short, long, value_name = "FILE", value_parser)]
input: PathBuf,
/// Path to output payload binary.
#[arg(short, long, value_name = "FILE", value_parser)]
output: PathBuf,
/// Path to output payload properties file.
#[arg(short, long, value_name = "FILE", value_parser)]
output_properties: Option<PathBuf>,
#[command(flatten)]
key: KeyGroup,
}
/// Display payload information.
#[derive(Debug, Parser)]
struct InfoCli {
/// Path to input payload file.
#[arg(short, long, value_name = "FILE", value_parser)]
input: PathBuf,
}
#[derive(Debug, Subcommand)]
enum PayloadCommand {
Unpack(UnpackCli),
Pack(PackCli),
Repack(RepackCli),
Info(InfoCli),
}
/// Inspect OTA payloads.
#[derive(Debug, Parser)]
pub struct PayloadCli {
#[command(subcommand)]
command: PayloadCommand,
/// Don't print payload header information.
#[arg(short, long, global = true)]
quiet: bool,
}
+275 -12
View File
@@ -1,5 +1,5 @@
/*
* SPDX-FileCopyrightText: 2023 Andrew Gunnerson
* SPDX-FileCopyrightText: 2023-2024 Andrew Gunnerson
* SPDX-License-Identifier: GPL-3.0-only
*/
@@ -9,6 +9,7 @@ use std::{
fs::{self, File, OpenOptions},
io::{self, BufReader, BufWriter, Read, Write},
path::{Path, PathBuf},
process::{Command, ExitStatus, Stdio},
time::Duration,
};
@@ -22,12 +23,16 @@ use cms::{
};
use pkcs8::{
pkcs5::{pbes2, scrypt},
DecodePrivateKey, EncodePrivateKey, EncodePublicKey, EncryptedPrivateKeyInfo, LineEnding,
PrivateKeyInfo,
DecodePrivateKey, DecodePublicKey, EncodePrivateKey, EncodePublicKey, EncryptedPrivateKeyInfo,
LineEnding, PrivateKeyInfo,
};
use rand::RngCore;
use rsa::{pkcs1v15::SigningKey, Pkcs1v15Sign, RsaPrivateKey, RsaPublicKey};
use sha2::Sha256;
use rsa::{
pkcs1v15::SigningKey, traits::PublicKeyParts, Pkcs1v15Sign, RsaPrivateKey, RsaPublicKey,
};
use serde::{Deserialize, Serialize};
use sha1::Sha1;
use sha2::{Digest, Sha256, Sha512};
use thiserror::Error;
use x509_cert::{
builder::{Builder, CertificateBuilder, Profile},
@@ -40,6 +45,20 @@ use x509_cert::{
#[derive(Debug, Error)]
pub enum Error {
#[error("Signature algorithm not supported: {0:?}")]
UnsupportedAlgorithm(SignatureAlgorithm),
#[error("RSA key size ({}) not supported", .0 * 8)]
UnsupportedKey(usize),
#[error("Invalid digest length ({0} bytes) for {1:?}")]
InvalidDigestLength(usize, SignatureAlgorithm),
#[error("Invalid signature length ({0} bytes) for {1:?}")]
InvalidSignatureLength(usize, SignatureAlgorithm),
#[error("Failed to run command: {0}")]
CommandSpawnFailed(String, #[source] io::Error),
#[error("Command failed with status: {1}: {0}")]
CommandExecutionFailed(String, ExitStatus),
#[error("Signature from signing helper does not match public key: {0:?}")]
SigningHelperBadSignature(PathBuf),
#[error("Passphrases do not match")]
ConfirmPassphrase,
#[error("Failed to read environment variable: {0:?}")]
@@ -54,6 +73,10 @@ pub enum Error {
SaveKeyEncrypted(#[source] pkcs8::Error),
#[error("Failed to save unencrypted private key")]
SaveKeyUnencrypted(#[source] pkcs8::Error),
#[error("Failed to RSA sign digest")]
RsaSign(#[source] rsa::Error),
#[error("Failed to RSA verify signature")]
RsaVerify(#[source] rsa::Error),
#[error("X509 error")]
X509(#[from] x509_cert::builder::Error),
#[error("SPKI error")]
@@ -68,6 +91,34 @@ pub enum Error {
type Result<T> = std::result::Result<T, Error>;
#[derive(Clone, Copy, Debug, Eq, PartialEq, Deserialize, Serialize)]
pub enum SignatureAlgorithm {
Sha1WithRsa,
Sha256WithRsa,
Sha512WithRsa,
}
impl SignatureAlgorithm {
/// Length of digest required by the signing algorithm.
pub fn digest_len(self) -> usize {
match self {
Self::Sha1WithRsa => Sha1::output_size(),
Self::Sha256WithRsa => Sha256::output_size(),
Self::Sha512WithRsa => Sha512::output_size(),
}
}
/// Compute the digest of the specified data.
pub fn hash(self, data: &[u8]) -> Vec<u8> {
match self {
Self::Sha1WithRsa => Sha1::digest(data).to_vec(),
Self::Sha256WithRsa => Sha256::digest(data).to_vec(),
Self::Sha512WithRsa => Sha512::digest(data).to_vec(),
}
}
}
#[derive(Clone)]
pub enum PassphraseSource {
Prompt(String),
EnvVar(OsString),
@@ -110,6 +161,181 @@ impl PassphraseSource {
}
}
fn check_key_size(size: usize) -> Result<()> {
// RustCrypto does not support 8192-bit keys.
if size > 4096 / 8 {
return Err(Error::UnsupportedKey(size));
}
Ok(())
}
/// Copied from rsa-0.9.6 since the function is not exported.
fn pkcs1v15_sign_pad(prefix: &[u8], hashed: &[u8], k: usize) -> rsa::Result<Vec<u8>> {
let hash_len = hashed.len();
let t_len = prefix.len() + hashed.len();
if k < t_len + 11 {
return Err(rsa::Error::MessageTooLong);
}
// EM = 0x00 || 0x01 || PS || 0x00 || T
let mut em = vec![0xff; k];
em[0] = 0;
em[1] = 1;
em[k - t_len - 1] = 0;
em[k - t_len..k - hash_len].copy_from_slice(prefix);
em[k - hash_len..k].copy_from_slice(hashed);
Ok(em)
}
#[derive(Clone)]
pub enum RsaSigningKey {
Internal(RsaPrivateKey),
External {
program: PathBuf,
public_key_file: PathBuf,
public_key: RsaPublicKey,
passphrase_source: PassphraseSource,
},
}
impl RsaSigningKey {
/// Size of key in bytes.
pub fn size(&self) -> usize {
match self {
Self::Internal(key) => key.size(),
Self::External { public_key, .. } => public_key.size(),
}
}
/// Get the public key portion of the signing key.
pub fn to_public_key(&self) -> RsaPublicKey {
match self {
RsaSigningKey::Internal(key) => key.to_public_key(),
RsaSigningKey::External { public_key, .. } => public_key.clone(),
}
}
/// Sign the digest with the specified signature algorithm.
pub fn sign(&self, algo: SignatureAlgorithm, digest: &[u8]) -> Result<Vec<u8>> {
if digest.len() != algo.digest_len() {
return Err(Error::InvalidDigestLength(digest.len(), algo));
}
check_key_size(self.size())?;
let scheme = match algo {
// We don't support signing with insecure algorithms.
SignatureAlgorithm::Sha1WithRsa => return Err(Error::UnsupportedAlgorithm(algo)),
SignatureAlgorithm::Sha256WithRsa => Pkcs1v15Sign::new::<Sha256>(),
SignatureAlgorithm::Sha512WithRsa => Pkcs1v15Sign::new::<Sha512>(),
};
match self {
Self::Internal(key) => key.sign(scheme, digest).map_err(Error::RsaSign),
Self::External {
program,
public_key,
public_key_file,
passphrase_source,
} => {
let key_bits = public_key.size() * 8;
let algo_str = match algo {
SignatureAlgorithm::Sha1WithRsa => unreachable!(),
SignatureAlgorithm::Sha256WithRsa => format!("SHA256_RSA{key_bits}"),
SignatureAlgorithm::Sha512WithRsa => format!("SHA512_RSA{key_bits}"),
};
let mut command = Command::new(program);
command.arg(algo_str);
command.arg(public_key_file);
match passphrase_source {
PassphraseSource::Prompt(_) => {}
PassphraseSource::EnvVar(v) => {
command.arg("env");
command.arg(v);
}
PassphraseSource::File(p) => {
command.arg("file");
command.arg(p);
}
}
command.stdin(Stdio::piped());
command.stdout(Stdio::piped());
command.stderr(Stdio::inherit());
let mut child = command
.spawn()
.map_err(|e| Error::CommandSpawnFailed(format!("{command:?}"), e))?;
// We don't bother with spawning a thread. The pipe capacity on
// all major OSs is significantly larger than the digest, so we
// don't risk deadlocking even if the process doesn't read from
// stdin.
//
// Pipe capacities:
// * Linux: 64 KiB
// * macOS: 4 KiB, 16 KiB (usually), or 64 KiB
// * Windows: 4 KiB
let padded_digest = pkcs1v15_sign_pad(&scheme.prefix, digest, public_key.size())?;
child.stdin.as_mut().unwrap().write_all(&padded_digest)?;
let child = child.wait_with_output()?;
if !child.status.success() {
return Err(Error::CommandExecutionFailed(
format!("{command:?}"),
child.status,
));
} else if child.stdout.len() != self.size() {
return Err(Error::InvalidSignatureLength(child.stdout.len(), algo));
}
// Check that the helper signed with the proper key.
if let Err(e) = self.to_public_key().verify_sig(algo, digest, &child.stdout) {
return match e {
Error::RsaVerify(_) => {
Err(Error::SigningHelperBadSignature(public_key_file.clone()))
}
e => Err(e),
};
}
Ok(child.stdout)
}
}
}
}
pub trait RsaPublicKeyExt {
fn verify_sig(&self, algo: SignatureAlgorithm, digest: &[u8], signature: &[u8]) -> Result<()>;
}
impl RsaPublicKeyExt for RsaPublicKey {
/// Verify the signature against the specified key.
fn verify_sig(&self, algo: SignatureAlgorithm, digest: &[u8], signature: &[u8]) -> Result<()> {
// Check this explicitly so we can provide a better error message.
if digest.len() != algo.digest_len() {
return Err(Error::InvalidDigestLength(digest.len(), algo));
}
check_key_size(self.size())?;
let scheme = match algo {
SignatureAlgorithm::Sha1WithRsa => Pkcs1v15Sign::new::<Sha1>(),
SignatureAlgorithm::Sha256WithRsa => Pkcs1v15Sign::new::<Sha256>(),
SignatureAlgorithm::Sha512WithRsa => Pkcs1v15Sign::new::<Sha512>(),
};
self.verify(scheme, digest, signature)
.map_err(Error::RsaVerify)
}
}
/// Generate an 4096-bit RSA key pair.
pub fn generate_rsa_key_pair() -> Result<RsaPrivateKey> {
let mut rng = rand::thread_rng();
@@ -164,6 +390,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 +402,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 {
@@ -225,6 +454,41 @@ pub fn write_pem_cert_file(path: &Path, cert: &Certificate) -> Result<()> {
write_pem_cert(writer, cert)
}
/// Read PEM-encoded PKCS8 public key from a reader.
pub fn read_pem_public_key(mut reader: impl Read) -> Result<RsaPublicKey> {
let mut data = String::new();
reader.read_to_string(&mut data)?;
let key = RsaPublicKey::from_public_key_pem(&data)?;
Ok(key)
}
/// Write PEM-encoded PKCS8 public key to a writer.
pub fn write_pem_public_key(mut writer: impl Write, key: &RsaPublicKey) -> Result<()> {
let data = key.to_public_key_pem(LineEnding::LF)?;
writer.write_all(data.as_bytes())?;
Ok(())
}
/// Read PEM-encoded PKCS8 public key from a file.
pub fn read_pem_public_key_file(path: &Path) -> Result<RsaPublicKey> {
let file = File::open(path)?;
let reader = BufReader::new(file);
read_pem_public_key(reader)
}
/// Write PEM-encoded PKCS8 public key to a file.
pub fn write_pem_public_key_file(path: &Path, key: &RsaPublicKey) -> Result<()> {
let file = File::create(path)?;
let writer = BufWriter::new(file);
write_pem_public_key(writer, key)
}
/// Read PEM-encoded PKCS8 private key from a reader.
pub fn read_pem_key(mut reader: impl Read, source: &PassphraseSource) -> Result<RsaPrivateKey> {
let mut data = String::new();
@@ -331,7 +595,7 @@ pub fn get_public_key(cert: &Certificate) -> Result<RsaPublicKey> {
}
/// Check if a certificate matches a private key.
pub fn cert_matches_key(cert: &Certificate, key: &RsaPrivateKey) -> Result<bool> {
pub fn cert_matches_key(cert: &Certificate, key: &RsaSigningKey) -> Result<bool> {
let public_key = get_public_key(cert)?;
Ok(key.to_public_key() == public_key)
@@ -369,12 +633,11 @@ pub fn get_cms_certs(sd: &SignedData) -> Vec<Certificate> {
/// a transport mechanism for a raw signature. Thus, we need to ensure that the
/// signature covers nothing but the raw data.
pub fn cms_sign_external(
key: &RsaPrivateKey,
key: &RsaSigningKey,
cert: &Certificate,
digest: &[u8],
) -> Result<ContentInfo> {
let scheme = Pkcs1v15Sign::new::<Sha256>();
let signature = key.sign(scheme, digest)?;
let signature = key.sign(SignatureAlgorithm::Sha256WithRsa, digest)?;
let digest_algorithm = AlgorithmIdentifierOwned {
oid: const_oid::db::rfc5912::ID_SHA_256,
+275 -366
View File
@@ -1,11 +1,12 @@
/*
* SPDX-FileCopyrightText: 2023 Andrew Gunnerson
* SPDX-FileCopyrightText: 2023-2024 Andrew Gunnerson
* SPDX-License-Identifier: GPL-3.0-only
*/
use std::{
cmp, fmt,
fmt,
io::{self, Cursor, Read, Seek, SeekFrom, Write},
ops::Range,
str,
sync::atomic::AtomicBool,
};
@@ -14,28 +15,28 @@ use bstr::ByteSlice;
use byteorder::{BigEndian, ReadBytesExt, WriteBytesExt};
use num_bigint_dig::{ModInverse, ToBigInt};
use num_traits::{Pow, ToPrimitive};
use rayon::prelude::{IntoParallelIterator, ParallelIterator};
use ring::digest::{Algorithm, Context};
use rsa::{traits::PublicKeyParts, BigUint, Pkcs1v15Sign, RsaPrivateKey, RsaPublicKey};
use rsa::{traits::PublicKeyParts, BigUint, RsaPublicKey};
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256, Sha512};
use thiserror::Error;
use crate::{
crypto::{self, RsaPublicKeyExt, RsaSigningKey, SignatureAlgorithm},
escape,
format::{
fec::{self, Fec},
hashtree::{self, HashTree},
padding,
},
stream::{
self, CountingReader, FromReader, ReadDiscardExt, ReadSeekReopen, ReadStringExt, ToWriter,
WriteSeekReopen, WriteStringExt, WriteZerosExt,
self, CountingReader, CountingWriter, FromReader, ReadDiscardExt, ReadSeekReopen,
ReadStringExt, ToWriter, WriteSeekReopen, WriteStringExt, WriteZerosExt,
},
util,
};
pub const VERSION_MAJOR: u32 = 1;
pub const VERSION_MINOR: u32 = 2;
pub const VERSION_MINOR: u32 = 3;
pub const VERSION_SUB: u32 = 0;
pub const FOOTER_VERSION_MAJOR: u32 = 1;
@@ -49,13 +50,30 @@ pub const FOOTER_MAGIC: [u8; 4] = *b"AVBf";
/// for early fail. No individual field can actually be this size.
pub const HEADER_MAX_SIZE: u64 = 64 * 1024;
/// Maximum hash tree size. The current limit equals the hash tree size for a
/// 4GiB image using SHA512 digests and a block size of 4096.
pub const HASH_TREE_MAX_SIZE: u64 = 68_177_920;
/// Maximum hash tree size. The current limit equals the hash tree size for an
/// 8GiB image using SHA256 digests and a block size of 4096. This is equal to:
///
/// ```rust
/// use avbroot::format::hashtree::HashTree;
/// let size = HashTree::new(4096, &ring::digest::SHA256, b"")
/// .compute_level_offsets(8 * 1024 * 1024 * 1024)
/// .unwrap()
/// .first()
/// .map(|r| r.end)
/// .unwrap_or(0);
/// ```
pub const HASH_TREE_MAX_SIZE: u64 = 67_637_248;
/// Maximum FEC data size. The current limit equals the FEC data size for a 4GiB
/// image using 2 parity bytes per codeword.
pub const FEC_DATA_MAX_SIZE: u64 = 33_959_936;
/// Maximum FEC data size. The current limit equals the FEC data size for an
/// 8GiB image using 2 parity bytes per codeword. This is equal to:
///
/// ```rust
/// use avbroot::format::fec::Fec;
/// let size = Fec::new(8 * 1024 * 1024 * 1024, 4096, 2)
/// .unwrap()
/// .fec_size();
/// ```
pub const FEC_DATA_MAX_SIZE: u64 = 67_911_680;
#[derive(Debug, Error)]
pub enum Error {
@@ -85,17 +103,10 @@ pub enum Error {
UnsupportedAlgorithm(AlgorithmType),
#[error("Hashing algorithm not supported: {0:?}")]
UnsupportedHashAlgorithm(String),
#[error("Incorrect key size ({key_size} bytes) for algorithm {algo:?} ({} bytes)", algo.public_key_len())]
IncorrectKeySize {
key_size: usize,
algo: AlgorithmType,
},
#[error("RSA key size (0) is not compatible with any AVB signing algorithm")]
#[error("Incorrect key size ({}) for algorithm {1:?}", .0 * 8)]
IncorrectKeySize(usize, AlgorithmType),
#[error("RSA key size ({}) is not compatible with any AVB signing algorithm", .0 * 8)]
UnsupportedKey(usize),
#[error("Expected root digest {expected}, but have {actual}")]
InvalidRootDigest { expected: String, actual: String },
#[error("Expected hash tree {expected}, but have {actual}")]
InvalidHashTree { expected: String, actual: String },
#[error("Hash tree does not immediately follow image data")]
HashTreeGap,
#[error("FEC data does not immediately follow hash tree")]
@@ -106,21 +117,25 @@ pub enum Error {
MismatchedFecBlockSizes { data: u32, hash: u32 },
#[error("Must have exactly one hash or hash tree descriptor")]
NoAppendedDescriptor,
#[error("Failed to RSA sign digest")]
RsaSign(#[source] rsa::Error),
#[error("Failed to RSA verify signature")]
RsaVerify(#[source] rsa::Error),
#[error("{0} byte image size is too small to fit header or footer")]
ImageSizeTooSmall(u64),
#[error("{0} byte image size is too small to fit header")]
TooSmallForHeader(u64),
#[error("{0} byte image size is too small to fit footer")]
TooSmallForFooter(u64),
#[error("Crypto error")]
Crypto(#[from] crypto::Error),
#[error("Hash tree error")]
HashTree(#[from] hashtree::Error),
#[error("FEC error")]
Fec(#[from] fec::Error),
#[error("RSA error")]
Rsa(#[from] rsa::Error),
#[error("I/O error")]
Io(#[from] io::Error),
}
type Result<T> = std::result::Result<T, Error>;
fn ring_algorithm(name: &str, for_verify: bool) -> Result<&'static Algorithm> {
pub(crate) fn ring_algorithm(name: &str, for_verify: bool) -> Result<&'static Algorithm> {
match name {
"sha1" if for_verify => Ok(&ring::digest::SHA1_FOR_LEGACY_USE_ONLY),
"sha256" => Ok(&ring::digest::SHA256),
@@ -138,6 +153,7 @@ pub enum AlgorithmType {
Sha512Rsa2048,
Sha512Rsa4096,
Sha512Rsa8192,
#[serde(untagged)]
Unknown(u32),
}
@@ -168,18 +184,24 @@ impl AlgorithmType {
}
}
pub fn hash_len(self) -> usize {
pub fn to_digest_algorithm(self) -> Option<SignatureAlgorithm> {
match self {
Self::None | Self::Unknown(_) => 0,
Self::Sha256Rsa2048 | Self::Sha256Rsa4096 | Self::Sha256Rsa8192 => {
Sha256::output_size()
Some(SignatureAlgorithm::Sha256WithRsa)
}
Self::Sha512Rsa2048 | Self::Sha512Rsa4096 | Self::Sha512Rsa8192 => {
Sha512::output_size()
Some(SignatureAlgorithm::Sha512WithRsa)
}
_ => None,
}
}
pub fn digest_len(self) -> usize {
self.to_digest_algorithm()
.map(|a| a.digest_len())
.unwrap_or_default()
}
pub fn signature_len(self) -> usize {
match self {
Self::None | Self::Unknown(_) => 0,
@@ -199,49 +221,36 @@ impl AlgorithmType {
}
pub fn hash(self, data: &[u8]) -> Vec<u8> {
match self {
Self::None | Self::Unknown(_) => vec![],
Self::Sha256Rsa2048 | Self::Sha256Rsa4096 | Self::Sha256Rsa8192 => {
Sha256::digest(data).to_vec()
}
Self::Sha512Rsa2048 | Self::Sha512Rsa4096 | Self::Sha512Rsa8192 => {
Sha512::digest(data).to_vec()
}
}
}
pub fn sign(self, key: &RsaPrivateKey, digest: &[u8]) -> Result<Vec<u8>> {
let signature = match self {
Self::None | Self::Unknown(_) => vec![],
Self::Sha256Rsa2048 | Self::Sha256Rsa4096 | Self::Sha256Rsa8192 => {
let scheme = Pkcs1v15Sign::new::<Sha256>();
key.sign(scheme, digest).map_err(Error::RsaSign)?
}
Self::Sha512Rsa2048 | Self::Sha512Rsa4096 | Self::Sha512Rsa8192 => {
let scheme = Pkcs1v15Sign::new::<Sha512>();
key.sign(scheme, digest).map_err(Error::RsaSign)?
}
let Some(algo) = self.to_digest_algorithm() else {
return vec![];
};
Ok(signature)
algo.hash(data)
}
pub fn sign(self, key: &RsaSigningKey, digest: &[u8]) -> Result<Vec<u8>> {
let Some(algo) = self.to_digest_algorithm() else {
return if self == Self::None {
Ok(vec![])
} else {
Err(Error::UnsupportedAlgorithm(self))
};
};
key.sign(algo, digest).map_err(|e| e.into())
}
pub fn verify(self, key: &RsaPublicKey, digest: &[u8], signature: &[u8]) -> Result<()> {
match self {
Self::None | Self::Unknown(_) => {}
Self::Sha256Rsa2048 | Self::Sha256Rsa4096 | Self::Sha256Rsa8192 => {
let scheme = Pkcs1v15Sign::new::<Sha256>();
key.verify(scheme, digest, signature)
.map_err(Error::RsaVerify)?;
}
Self::Sha512Rsa2048 | Self::Sha512Rsa4096 | Self::Sha512Rsa8192 => {
let scheme = Pkcs1v15Sign::new::<Sha512>();
key.verify(scheme, digest, signature)
.map_err(Error::RsaVerify)?;
}
}
let Some(algo) = self.to_digest_algorithm() else {
return if self == Self::None {
Ok(())
} else {
Err(Error::UnsupportedAlgorithm(self))
};
};
Ok(())
key.verify_sig(algo, digest, signature)
.map_err(|e| e.into())
}
}
@@ -379,155 +388,8 @@ impl fmt::Debug for HashTreeDescriptor {
}
impl HashTreeDescriptor {
/// Calculate the hash tree digests for a single level of the tree. If the
/// reader's position is block-aligned and `image_size` is a multiple of the
/// block size, then this function can also be used to calculate the digests
/// for a portion of a level.
///
/// NOTE: The result is **not** padded to the block size.
fn hash_one_level(
mut reader: impl Read,
mut image_size: u64,
block_size: u32,
algorithm: &'static Algorithm,
salt: &[u8],
cancel_signal: &AtomicBool,
) -> io::Result<Vec<u8>> {
// Each digest must be a power of 2.
let digest_padding = algorithm.output_len().next_power_of_two() - algorithm.output_len();
let mut buf = vec![0u8; block_size as usize];
let mut result = vec![];
while image_size > 0 {
stream::check_cancel(cancel_signal)?;
let n = image_size.min(buf.len() as u64) as usize;
reader.read_exact(&mut buf[..n])?;
// For undersized blocks, we still hash the whole buffer, except
// with padding.
buf[n..].fill(0);
let mut context = Context::new(algorithm);
context.update(salt);
context.update(&buf);
// Add the digest to the tree level. Each tree node must be a power
// of two.
let digest = context.finish();
result.extend(digest.as_ref());
result.resize(result.len() + digest_padding, 0);
image_size -= n as u64;
}
Ok(result)
}
/// Calls [`Self::hash_one_level()`] in parallel.
///
/// NOTE: The result is **not** padded to the block size.
fn hash_one_level_parallel(
input: &(dyn ReadSeekReopen + Sync),
image_size: u64,
block_size: u32,
algorithm: &'static Algorithm,
salt: &[u8],
cancel_signal: &AtomicBool,
) -> io::Result<Vec<u8>> {
assert!(
image_size > block_size as u64,
"Images smaller than block size must use a normal hash",
);
// Parallelize in 16 MiB chunks to avoid too much seek thrashing.
let chunk_size = padding::round(16 * 1024 * 1024, u64::from(block_size)).unwrap();
let chunk_count = image_size / chunk_size + u64::from(image_size % chunk_size != 0);
let pieces = (0..chunk_count)
.into_par_iter()
.map(|c| -> io::Result<Vec<u8>> {
let start = c * chunk_size;
let size = chunk_size.min(image_size - start);
let mut reader = input.reopen_boxed()?;
reader.seek(SeekFrom::Start(start))?;
Self::hash_one_level(reader, size, block_size, algorithm, salt, cancel_signal)
})
.collect::<io::Result<Vec<_>>>()?;
Ok(pieces.into_iter().flatten().collect())
}
/// Calculate the hash tree for the given input in parallel.
fn calculate_hash_tree(
input: &(dyn ReadSeekReopen + Sync),
image_size: u64,
block_size: u32,
algorithm: &'static Algorithm,
salt: &[u8],
cancel_signal: &AtomicBool,
) -> io::Result<(Vec<u8>, Vec<u8>)> {
// Small files are hashed directly, exactly like a hash descriptor.
if image_size <= u64::from(block_size) {
let mut reader = input.reopen_boxed()?;
let mut buf = vec![0u8; block_size as usize];
reader.read_exact(&mut buf)?;
let mut context = Context::new(algorithm);
context.update(salt);
context.update(&buf);
let digest = context.finish();
return Ok((digest.as_ref().to_vec(), vec![]));
}
// Large files use the hash tree.
let mut levels = Vec::<Vec<u8>>::new();
let mut level_size = image_size;
while level_size > u64::from(block_size) {
let mut level = if let Some(prev_level) = levels.last() {
// Hash the previous level.
Self::hash_one_level(
Cursor::new(prev_level),
level_size,
block_size,
algorithm,
salt,
cancel_signal,
)?
} else {
// Initially read from file.
Self::hash_one_level_parallel(
input,
level_size,
block_size,
algorithm,
salt,
cancel_signal,
)?
};
// Pad to the block size.
level.resize(padding::round(level.len(), block_size as usize).unwrap(), 0);
level_size = level.len() as u64;
levels.push(level);
}
// Calculate the root hash.
let mut context = Context::new(algorithm);
context.update(salt);
context.update(levels.last().unwrap());
let root_hash = context.finish().as_ref().to_vec();
// The tree is oriented such that the leaves are at the end.
let hash_tree = levels.into_iter().rev().flatten().collect();
Ok((root_hash, hash_tree))
}
pub const FLAG_DO_NOT_USE_AB: u32 = 1 << 0;
pub const FLAG_CHECK_AT_MOST_ONCE: u32 = 1 << 1;
/// Ensure that the image data is immediately followed by the hash tree and
/// then the FEC data.
@@ -582,10 +444,15 @@ impl HashTreeDescriptor {
/// the same file. It is guaranteed that every thread will read and write
/// disjoint file offsets.
///
/// Due to the nature of the file access patterns needed to generate the
/// hash tree and FEC data, the entire file will be read twice. However, if
/// If `ranges` is [`Option::None`], then the hash tree and FEC data are
/// updated for the whole while. Due to the nature of the file access
/// patterns, the entire file will be read twice. However, if
/// [`Self::fec_num_roots`] is 0, no FEC data will be computed nor written.
///
/// If `ranges` is specified, only the hash tree and FEC data corresponding
/// to those ranges are updated. It may be necessary read a bit more data
/// that what is specified in order to perform the computations.
///
/// The fields in this instance are updated atomically. No fields are
/// updated if an error occurs. The input file can be restored back to its
/// original state by truncating it to [`Self::image_size`].
@@ -593,28 +460,44 @@ impl HashTreeDescriptor {
&mut self,
input: &(dyn ReadSeekReopen + Sync),
output: &(dyn WriteSeekReopen + Sync),
ranges: Option<&[Range<u64>]>,
cancel_signal: &AtomicBool,
) -> Result<()> {
let algorithm = ring_algorithm(&self.hash_algorithm, false)?;
let (root_digest, hash_tree) = Self::calculate_hash_tree(
input,
self.image_size,
self.data_block_size,
algorithm,
&self.salt,
cancel_signal,
)?;
let hash_tree = HashTree::new(self.data_block_size, algorithm, &self.salt);
let (root_digest, hash_tree_data) = match ranges {
Some(r) => {
let mut reader = input.reopen_boxed()?;
reader.seek(SeekFrom::Start(self.tree_offset))?;
if hash_tree.len() > HASH_TREE_MAX_SIZE as usize {
let mut hash_tree_data = vec![0u8; self.tree_size as usize];
reader
.read_exact(&mut hash_tree_data)
.map_err(|e| Error::ReadFieldError("hash_tree", e))?;
let root_digest = hash_tree.update(
input,
self.image_size,
r,
&mut hash_tree_data,
cancel_signal,
)?;
(root_digest, hash_tree_data)
}
None => hash_tree.generate(input, self.image_size, cancel_signal)?,
};
if hash_tree_data.len() > HASH_TREE_MAX_SIZE as usize {
return Err(Error::FieldOutOfBounds("tree_size"));
}
let tree_size = hash_tree.len() as u64;
let tree_size = hash_tree_data.len() as u64;
let mut writer = output.reopen_boxed()?;
writer.seek(SeekFrom::Start(self.image_size))?;
writer
.write_all(&hash_tree)
.write_all(&hash_tree_data)
.map_err(|e| Error::WriteFieldError("hash_tree", e))?;
// The FEC data section is optional.
@@ -631,10 +514,31 @@ impl HashTreeDescriptor {
.to_u8()
.ok_or_else(|| Error::FieldOutOfBounds("fec_num_roots"))?;
// The FEC covers the hash tree as well.
let fec = Fec::new(self.image_size + tree_size, self.data_block_size, parity)?;
let fec_data = match ranges {
Some(r) => {
let mut r_with_hash_tree = r.to_vec();
r_with_hash_tree.push(self.tree_offset..self.tree_offset + tree_size);
let fec_data = fec.generate(input, cancel_signal)?;
let (fec, fec_size) = self.get_fec()?;
let mut reader = input.reopen_boxed()?;
reader.seek(SeekFrom::Start(self.fec_offset))?;
let mut fec_data = vec![0u8; fec_size];
reader
.read_exact(&mut fec_data)
.map_err(|e| Error::ReadFieldError("fec_data", e))?;
fec.update(input, &r_with_hash_tree, &mut fec_data, cancel_signal)?;
fec_data
}
None => {
// The FEC covers the hash tree as well.
let fec = Fec::new(self.image_size + tree_size, self.data_block_size, parity)?;
fec.generate(input, cancel_signal)?
}
};
let fec_size = fec_data
.len()
.to_u64()
@@ -672,40 +576,23 @@ impl HashTreeDescriptor {
return Err(Error::FieldOutOfBounds("tree_size"));
}
let (actual_root_digest, actual_hash_tree) = Self::calculate_hash_tree(
input,
self.image_size,
self.data_block_size,
algorithm,
&self.salt,
cancel_signal,
)?;
if self.root_digest != actual_root_digest {
return Err(Error::InvalidRootDigest {
expected: hex::encode(&self.root_digest),
actual: hex::encode(actual_root_digest),
});
}
let mut reader = input.reopen_boxed()?;
reader.seek(SeekFrom::Start(self.tree_offset))?;
let mut hash_tree = vec![0u8; self.tree_size as usize];
let mut hash_tree_data = vec![0u8; self.tree_size as usize];
reader
.read_exact(&mut hash_tree)
.read_exact(&mut hash_tree_data)
.map_err(|e| Error::ReadFieldError("hash_tree", e))?;
if hash_tree != actual_hash_tree {
// These are multiple megabytes, so only report the hashes.
let expected = ring::digest::digest(algorithm, &hash_tree);
let actual = ring::digest::digest(algorithm, &actual_hash_tree);
let hash_tree = HashTree::new(self.data_block_size, algorithm, &self.salt);
return Err(Error::InvalidHashTree {
expected: hex::encode(expected),
actual: hex::encode(actual),
});
}
hash_tree.verify(
input,
self.image_size,
&self.root_digest,
&hash_tree_data,
cancel_signal,
)?;
// The FEC data section is optional.
if self.fec_num_roots != 0 {
@@ -942,10 +829,11 @@ impl HashDescriptor {
let digest = self.calculate(reader, true, cancel_signal)?;
if self.root_digest != digest.as_ref() {
return Err(Error::InvalidRootDigest {
return Err(hashtree::Error::InvalidRootDigest {
expected: hex::encode(&self.root_digest),
actual: hex::encode(digest),
});
}
.into());
}
Ok(())
@@ -1053,6 +941,11 @@ pub struct KernelCmdlineDescriptor {
pub cmdline: String,
}
impl KernelCmdlineDescriptor {
pub const FLAG_USE_ONLY_IF_HASHTREE_NOT_DISABLED: u32 = 1 << 0;
pub const FLAG_USE_ONLY_IF_HASHTREE_DISABLED: u32 = 1 << 1;
}
impl DescriptorTag for KernelCmdlineDescriptor {
const TAG: u64 = 3;
}
@@ -1101,8 +994,13 @@ pub struct ChainPartitionDescriptor {
pub partition_name: String,
#[serde(with = "hex")]
pub public_key: Vec<u8>,
pub flags: u32,
#[serde(with = "hex")]
pub reserved: [u8; 64],
pub reserved: [u8; 60],
}
impl ChainPartitionDescriptor {
pub const FLAG_DO_NOT_USE_AB: u32 = 1 << 0;
}
impl fmt::Debug for ChainPartitionDescriptor {
@@ -1111,6 +1009,7 @@ impl fmt::Debug for ChainPartitionDescriptor {
.field("rollback_index_location", &self.rollback_index_location)
.field("partition_name", &self.partition_name)
.field("public_key", &hex::encode(&self.public_key))
.field("flags", &self.flags)
.field("reserved", &hex::encode(self.reserved))
.finish()
}
@@ -1134,7 +1033,9 @@ impl<R: Read> FromReader<R> for ChainPartitionDescriptor {
return Err(Error::FieldOutOfBounds("public_key_len"));
}
let mut reserved = [0u8; 64];
let flags = reader.read_u32::<BigEndian>()?;
let mut reserved = [0u8; 60];
reader.read_exact(&mut reserved)?;
// Not NULL-terminated.
@@ -1149,6 +1050,7 @@ impl<R: Read> FromReader<R> for ChainPartitionDescriptor {
rollback_index_location,
partition_name,
public_key,
flags,
reserved,
};
@@ -1169,6 +1071,7 @@ impl<W: Write> ToWriter<W> for ChainPartitionDescriptor {
writer.write_u32::<BigEndian>(self.rollback_index_location)?;
writer.write_u32::<BigEndian>(self.partition_name.len() as u32)?;
writer.write_u32::<BigEndian>(self.public_key.len() as u32)?;
writer.write_u32::<BigEndian>(self.flags)?;
writer.write_all(&self.reserved)?;
writer.write_all(self.partition_name.as_bytes())?;
writer.write_all(&self.public_key)?;
@@ -1193,6 +1096,17 @@ pub enum Descriptor {
}
impl Descriptor {
pub fn type_name(&self) -> &'static str {
match self {
Self::Property(_) => "Property",
Self::HashTree(_) => "HashTree",
Self::Hash(_) => "Hash",
Self::KernelCmdline(_) => "KernelCmdline",
Self::ChainPartition(_) => "ChainPartition",
Self::Unknown { .. } => "Unknown",
}
}
pub fn partition_name(&self) -> Option<&str> {
match self {
Self::HashTree(d) => Some(&d.partition_name),
@@ -1538,7 +1452,7 @@ impl Header {
result.ok_or(Error::NoAppendedDescriptor)
}
pub fn set_algo_for_key(&mut self, key: &RsaPrivateKey) -> Result<()> {
pub fn set_algo_for_key(&mut self, key: &RsaSigningKey) -> Result<()> {
let key_raw = encode_public_key(&key.to_public_key())?;
for algo in [AlgorithmType::Sha256Rsa2048, AlgorithmType::Sha256Rsa4096] {
@@ -1558,30 +1472,17 @@ impl Header {
self.public_key_metadata.clear();
}
pub fn sign(&mut self, key: &RsaPrivateKey) -> Result<()> {
pub fn sign(&mut self, key: &RsaSigningKey) -> Result<()> {
let key_raw = encode_public_key(&key.to_public_key())?;
// RustCrypto does not support 8192-bit keys.
match self.algorithm_type {
AlgorithmType::Sha256Rsa8192
| AlgorithmType::Sha512Rsa8192
| AlgorithmType::Unknown(_) => {
return Err(Error::UnsupportedAlgorithm(self.algorithm_type));
}
_ => {}
}
if key_raw.len() != self.algorithm_type.public_key_len() {
return Err(Error::IncorrectKeySize {
key_size: key_raw.len(),
algo: self.algorithm_type,
});
return Err(Error::IncorrectKeySize(key.size(), self.algorithm_type));
}
// The public key and the sizes of the hash and signature are included
// in the data that's about to be signed.
self.public_key = key_raw;
self.hash.resize(self.algorithm_type.hash_len(), 0);
self.hash.resize(self.algorithm_type.digest_len(), 0);
self.signature
.resize(self.algorithm_type.signature_len(), 0);
@@ -1602,18 +1503,16 @@ impl Header {
/// and return the public key. If the header is not signed, then `None` is
/// returned.
pub fn verify(&self) -> Result<Option<RsaPublicKey>> {
// RustCrypto does not support 8192-bit keys.
match self.algorithm_type {
AlgorithmType::None => return Ok(None),
a @ AlgorithmType::Sha256Rsa8192
| a @ AlgorithmType::Sha512Rsa8192
| a @ AlgorithmType::Unknown(_) => return Err(Error::UnsupportedAlgorithm(a)),
_ => {}
}
// Reconstruct the public key.
let public_key = decode_public_key(&self.public_key)?;
if self.public_key.len() != self.algorithm_type.public_key_len() {
return Err(Error::IncorrectKeySize(
public_key.size(),
self.algorithm_type,
));
}
let mut without_auth_writer = Cursor::new(Vec::new());
self.to_writer_internal(&mut without_auth_writer, true)?;
let without_auth = without_auth_writer.into_inner();
@@ -1902,8 +1801,7 @@ pub fn decode_public_key(data: &[u8]) -> Result<RsaPublicKey> {
reader.read_exact(&mut modulus_raw)?;
let modulus = BigUint::from_bytes_be(&modulus_raw);
let public_key =
RsaPublicKey::new(modulus, BigUint::from(65537u32)).map_err(Error::RsaVerify)?;
let public_key = RsaPublicKey::new(modulus, BigUint::from(65537u32))?;
Ok(public_key)
}
@@ -1930,84 +1828,95 @@ pub fn load_image(mut reader: impl Read + Seek) -> Result<(Header, Option<Footer
Ok((header, footer, image_size))
}
/// Write a vbmeta header to the specified writer. If a footer is specified, it
/// will be used as the basis of the newly written footer, with the original
/// image size, vbmeta header offset, and vbmeta header size fields updated
/// appropriately.
///
/// The writer must not have an existing vbmeta header or footer.
fn write_image_internal(
mut writer: impl Write + Seek,
header: &Header,
footer: Option<&mut Footer>,
image_size: Option<u64>,
block_size: u64,
) -> Result<()> {
let eof_image_size = writer.seek(SeekFrom::End(0))?;
// The header must be block-aligned.
let vbmeta_offset = if block_size > 0 {
let padding_size = padding::write_zeros(&mut writer, block_size)?;
eof_image_size
.checked_add(padding_size)
.ok_or_else(|| Error::FieldOutOfBounds("vbmeta_offset"))?
} else {
eof_image_size
};
header.to_writer(&mut writer)?;
let vbmeta_end = writer.stream_position()?;
if let Some(s) = image_size {
let footer_space = if footer.is_some() {
cmp::max(block_size, Footer::SIZE as u64)
} else {
0
};
if s < footer_space || vbmeta_end > s - footer_space {
return Err(Error::ImageSizeTooSmall(s));
}
}
if block_size > 0 {
padding::write_zeros(&mut writer, block_size)?;
}
if let Some(f) = footer {
let footer_offset = image_size.unwrap() - Footer::SIZE as u64;
writer.seek(SeekFrom::Start(footer_offset))?;
let original_image_size = match header.appended_descriptor()? {
AppendedDescriptorRef::HashTree(d) => d.image_size,
AppendedDescriptorRef::Hash(d) => d.image_size,
};
f.original_image_size = original_image_size;
f.vbmeta_offset = vbmeta_offset;
f.vbmeta_size = vbmeta_end - vbmeta_offset;
f.to_writer(&mut writer)?;
}
Ok(())
}
/// Write a vbmeta header to the specified writer. This is meant for writing
/// vbmeta partition images, not appended vbmeta images. The writer must refer
/// to an empty file.
pub fn write_root_image(writer: impl Write + Seek, header: &Header, block_size: u64) -> Result<()> {
write_image_internal(writer, header, None, None, block_size)
/// to an empty file. Returns the size of the new file.
pub fn write_root_image(writer: impl Write, header: &Header, block_size: u64) -> Result<u64> {
let mut counting_writer = CountingWriter::new(writer);
header.to_writer(&mut counting_writer)?;
padding::write_zeros(&mut counting_writer, block_size)?;
Ok(counting_writer.stream_position()?)
}
/// Write a vbmeta header and footer to the specified writer. This is meant for
/// appending vbmeta data to existing partition data, not writing vbmeta images.
/// If `image_size` is specified, then the writer is guaranteed to not grow
/// past that size and an error is returned if the header and footer won't fit.
/// Otherwise, the writer will grow to the necessary size. Returns the size of
/// the new file.
pub fn write_appended_image(
writer: impl Write + Seek,
mut writer: impl Write + Seek,
header: &Header,
footer: &mut Footer,
image_size: u64,
) -> Result<()> {
image_size: Option<u64>,
) -> Result<u64> {
// avbtool hardcodes a 4096 block size for appended non-sparse images.
write_image_internal(writer, header, Some(footer), Some(image_size), 4096)
const BLOCK_SIZE: u64 = 4096;
// Logical image size, excluding the AVB header and footer.
let logical_image_size = match header.appended_descriptor()? {
AppendedDescriptorRef::HashTree(d) => d
.image_size
.checked_add(d.tree_size)
.and_then(|s| s.checked_add(d.fec_size))
.ok_or_else(|| Error::FieldOutOfBounds("logical_image_size"))?,
AppendedDescriptorRef::Hash(d) => d.image_size,
};
writer.seek(SeekFrom::Start(logical_image_size))?;
// The header start offset must be block aligned.
let header_offset = {
let padding_size = padding::write_zeros(&mut writer, BLOCK_SIZE)?;
logical_image_size
.checked_add(padding_size)
.ok_or_else(|| Error::FieldOutOfBounds("header_offset"))?
};
// The header lives at the beginning of the empty space.
let mut header_buf = Cursor::new(Vec::new());
header.to_writer(&mut header_buf)?;
let header_size = header_buf.stream_position()?;
let header_padding = padding::write_zeros(&mut header_buf, BLOCK_SIZE)?;
let header_end_padded = header_offset
.checked_add(header_size)
.and_then(|s| s.checked_add(header_padding))
.ok_or_else(|| Error::FieldOutOfBounds("header_end_padded"))?;
if let Some(s) = image_size {
if header_end_padded > s {
return Err(Error::TooSmallForHeader(s));
}
}
writer.write_all(&header_buf.into_inner())?;
// The footer lives in its own separate block at the end of the empty space.
let footer_end = if let Some(s) = image_size {
if s - header_end_padded < BLOCK_SIZE {
return Err(Error::TooSmallForFooter(s));
}
s
} else {
header_end_padded
.checked_add(BLOCK_SIZE)
.ok_or_else(|| Error::FieldOutOfBounds("footer_end"))?
};
let footer_offset = footer_end - Footer::SIZE as u64;
writer.seek(SeekFrom::Start(footer_offset))?;
footer.original_image_size = match header.appended_descriptor()? {
AppendedDescriptorRef::HashTree(d) => d.image_size,
AppendedDescriptorRef::Hash(d) => d.image_size,
};
footer.vbmeta_offset = header_offset;
footer.vbmeta_size = header_size;
footer.to_writer(&mut writer)?;
Ok(footer_end)
}
+3 -3
View File
@@ -1,5 +1,5 @@
/*
* SPDX-FileCopyrightText: 2023 Andrew Gunnerson
* SPDX-FileCopyrightText: 2023-2024 Andrew Gunnerson
* SPDX-License-Identifier: GPL-3.0-only
*/
@@ -12,11 +12,11 @@ use std::{
use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt};
use num_traits::ToPrimitive;
use ring::digest::Context;
use rsa::RsaPrivateKey;
use serde::{Deserialize, Serialize};
use thiserror::Error;
use crate::{
crypto::RsaSigningKey,
format::{
avb::{self, Descriptor, Header},
padding,
@@ -765,7 +765,7 @@ impl BootImageV3Through4 {
/// Sign the boot image with a legacy VTS signature. Returns true if the
/// image was successfully signed. Returns false if there's no vbmeta
/// structure to sign in [`V4Extra::signature`].
pub fn sign(&mut self, key: &RsaPrivateKey) -> Result<bool> {
pub fn sign(&mut self, key: &RsaSigningKey) -> Result<bool> {
let mut context = Context::new(&ring::digest::SHA256);
let image_size;
+27 -2
View File
@@ -7,17 +7,25 @@ use std::io::{self, Read, Seek, Write};
use byteorder::{LittleEndian, WriteBytesExt};
use flate2::{read::GzDecoder, write::GzEncoder, Compression};
use liblzma::{
read::XzDecoder,
stream::{Check, Stream},
write::XzEncoder,
};
use lz4_flex::frame::FrameDecoder;
use serde::{Deserialize, Serialize};
use thiserror::Error;
static GZIP_MAGIC: &[u8; 2] = b"\x1f\x8b";
static LZ4_LEGACY_MAGIC: &[u8; 4] = b"\x02\x21\x4c\x18";
static XZ_MAGIC: &[u8; 6] = b"\xfd\x37\x7a\x58\x5a\x00";
#[derive(Debug, Error)]
pub enum Error {
#[error("Unknown compression format")]
UnknownFormat,
#[error("XZ stream error")]
XzStream(#[from] liblzma::stream::Error),
#[error("I/O error")]
Io(#[from] io::Error),
}
@@ -102,25 +110,29 @@ pub enum CompressedFormat {
None,
Gzip,
Lz4Legacy,
Xz,
}
pub enum CompressedReader<R: Read> {
None(R),
Gzip(GzDecoder<R>),
Lz4(FrameDecoder<R>),
Xz(XzDecoder<R>),
}
impl<R: Read + Seek> CompressedReader<R> {
pub fn new(mut reader: R, raw_if_unknown: bool) -> Result<Self> {
let mut magic = [0u8; 4];
let mut magic = [0u8; 6];
reader.read_exact(&mut magic)?;
reader.rewind()?;
if &magic[0..2] == GZIP_MAGIC {
Ok(Self::Gzip(GzDecoder::new(reader)))
} else if &magic == LZ4_LEGACY_MAGIC {
} else if &magic[0..4] == LZ4_LEGACY_MAGIC {
Ok(Self::Lz4(FrameDecoder::new(reader)))
} else if &magic == XZ_MAGIC {
Ok(Self::Xz(XzDecoder::new(reader)))
} else if raw_if_unknown {
Ok(Self::None(reader))
} else {
@@ -133,6 +145,7 @@ impl<R: Read + Seek> CompressedReader<R> {
Self::None(_) => CompressedFormat::None,
Self::Gzip(_) => CompressedFormat::Gzip,
Self::Lz4(_) => CompressedFormat::Lz4Legacy,
Self::Xz(_) => CompressedFormat::Xz,
}
}
@@ -141,6 +154,7 @@ impl<R: Read + Seek> CompressedReader<R> {
Self::None(r) => r,
Self::Gzip(r) => r.into_inner(),
Self::Lz4(r) => r.into_inner(),
Self::Xz(r) => r.into_inner(),
}
}
}
@@ -151,6 +165,7 @@ impl<R: Read> Read for CompressedReader<R> {
Self::None(r) => r.read(buf),
Self::Gzip(r) => r.read(buf),
Self::Lz4(r) => r.read(buf),
Self::Xz(r) => r.read(buf),
}
}
}
@@ -159,6 +174,7 @@ pub enum CompressedWriter<W: Write> {
None(W),
Gzip(GzEncoder<W>),
Lz4Legacy(Lz4LegacyEncoder<W>),
Xz(XzEncoder<W>),
}
impl<W: Write> CompressedWriter<W> {
@@ -169,6 +185,11 @@ impl<W: Write> CompressedWriter<W> {
Ok(Self::Gzip(GzEncoder::new(writer, Compression::default())))
}
CompressedFormat::Lz4Legacy => Ok(Self::Lz4Legacy(Lz4LegacyEncoder::new(writer)?)),
CompressedFormat::Xz => {
// Some kernels are compiled without support for the default CRC64.
let stream = Stream::new_easy_encoder(6, Check::Crc32)?;
Ok(Self::Xz(XzEncoder::new_stream(writer, stream)))
}
}
}
@@ -177,6 +198,7 @@ impl<W: Write> CompressedWriter<W> {
Self::None(_) => CompressedFormat::None,
Self::Gzip(_) => CompressedFormat::Gzip,
Self::Lz4Legacy(_) => CompressedFormat::Lz4Legacy,
Self::Xz(_) => CompressedFormat::Xz,
}
}
@@ -185,6 +207,7 @@ impl<W: Write> CompressedWriter<W> {
Self::None(w) => Ok(w),
Self::Gzip(w) => w.finish(),
Self::Lz4Legacy(w) => w.finish(),
Self::Xz(w) => w.finish(),
}
}
}
@@ -195,6 +218,7 @@ impl<W: Write> Write for CompressedWriter<W> {
Self::None(w) => w.write(buf),
Self::Gzip(w) => w.write(buf),
Self::Lz4Legacy(w) => w.write(buf),
Self::Xz(w) => w.write(buf),
}
}
@@ -203,6 +227,7 @@ impl<W: Write> Write for CompressedWriter<W> {
Self::None(w) => w.flush(),
Self::Gzip(w) => w.flush(),
Self::Lz4Legacy(w) => w.flush(),
Self::Xz(w) => w.flush(),
}
}
}
+117 -14
View File
@@ -4,8 +4,10 @@
*/
use std::{
collections::HashSet,
fmt,
io::{self, Cursor, Read, Seek, SeekFrom, Write},
ops::Range,
sync::atomic::AtomicBool,
};
@@ -20,7 +22,7 @@ use thiserror::Error;
use crate::{
format::verityrs,
stream::{self, FromReader, ReadSeekReopen, ToWriter, WriteSeekReopen, WriteZerosExt},
util::NumBytes,
util::{self, NumBytes},
};
// Not to be confused with the 255-byte RS block size.
@@ -111,11 +113,6 @@ impl Codeword {
}
}
/// Since Rust's built-in .div_ceil() is still nightly-only.
fn div_ceil(dividend: u64, divisor: u64) -> u64 {
dividend / divisor + u64::from(dividend % divisor != 0)
}
/// A type for performing FEC generation, verification, and error correction for
/// a specific file size and Reed Solomon configuration. The implementation uses
/// dm-verity's interleaving access pattern.
@@ -169,8 +166,8 @@ impl Fec {
return Err(Error::UnsupportedParity(parity));
}
let blocks = div_ceil(file_size, u64::from(block_size));
let rounds = div_ceil(blocks, u64::from(rs_k));
let blocks = file_size.div_ceil(u64::from(block_size));
let rounds = blocks.div_ceil(u64::from(rs_k));
// Check upfront so we don't need to do checked multiplication later.
rounds
@@ -199,7 +196,7 @@ impl Fec {
/// Get the size of the FEC data needed to cover the entire file.
#[inline]
fn fec_size(&self) -> usize {
pub fn fec_size(&self) -> usize {
usize::from(self.parity()) * self.rounds as usize * self.block_size as usize
}
@@ -211,6 +208,34 @@ impl Fec {
offset / rs_k + offset % rs_k * self.rounds * u64::from(self.block_size)
}
/// Get the rounds that correspond to the specified ranges.
fn rounds_for_ranges(&self, ranges: &[Range<u64>]) -> Result<HashSet<u64>> {
let ranges = util::merge_overlapping(ranges);
if let Some(last) = ranges.last() {
if last.end > self.file_size {
return Err(Error::FieldOutOfBounds("ranges"));
}
}
let block_size = u64::from(self.block_size);
let mut result = HashSet::new();
for range in ranges {
let start_block = range.start / block_size;
let end_block = if range.end % block_size == 0 {
range.end / block_size
} else {
range.end.div_ceil(block_size)
};
for block in start_block..end_block {
result.insert(block % self.rounds);
}
}
Ok(result)
}
/// Read a raw sequential block from the backing file, starting at offset
/// `offset` in the interleaved view. This reads a horizontal block-aligned
/// slice in the file offset grid.
@@ -438,6 +463,41 @@ impl Fec {
Ok(fec)
}
/// Update FEC data coreesponding to the specified file ranges.
///
/// This function is multithreaded and uses rayon's global thread pool.
pub fn update(
&self,
input: &(dyn ReadSeekReopen + Sync),
ranges: &[Range<u64>],
fec: &mut [u8],
cancel_signal: &AtomicBool,
) -> Result<()> {
let fec_size = self.fec_size();
if fec.len() != fec_size {
return Err(Error::InvalidFecSize {
input: self.file_size,
expected: fec_size,
actual: fec.len(),
});
}
let rounds_to_update = self.rounds_for_ranges(ranges)?;
fec.par_chunks_exact_mut(fec_size / self.rounds as usize)
.enumerate()
.filter(|(round, _)| rounds_to_update.contains(&(*round as u64)))
.map(|(round, buf)| -> Result<()> {
stream::check_cancel(cancel_signal)?;
let reader = input.reopen_boxed()?;
self.generate_one_round(reader, round as u64, buf)
})
.collect::<Result<()>>()?;
Ok(())
}
/// Verify that the file contains no errors. This is significantly faster
/// than [`Self::repair()`] if only error detection, not correction, is
/// needed.
@@ -530,7 +590,7 @@ pub struct FecImage {
impl fmt::Debug for FecImage {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Fec")
f.debug_struct("FecImage")
.field("fec", &NumBytes(self.fec.len()))
.field("data_size", &self.data_size)
.field("parity", &self.parity)
@@ -560,6 +620,17 @@ impl FecImage {
})
}
/// Update FEC data coreesponding to the specified file ranges.
pub fn update(
&mut self,
input: &(dyn ReadSeekReopen + Sync),
ranges: &[Range<u64>],
cancel_signal: &AtomicBool,
) -> Result<()> {
let fec = Fec::new(self.data_size, FEC_BLOCK_SIZE as u32, self.parity)?;
fec.update(input, ranges, &mut self.fec, cancel_signal)
}
/// Check that a file contains no errors. This is significantly faster than
/// [`Self::repair()`] if performing a repair is not necessary.
pub fn verify(
@@ -731,6 +802,31 @@ mod tests {
use super::*;
#[test]
fn rounds_for_ranges() {
let size = 2 * 253 * 4096;
let fec = Fec::new(size, 4096, 2).unwrap();
assert_eq!(fec.rounds_for_ranges(&[0..0]).unwrap(), HashSet::new());
assert_eq!(
fec.rounds_for_ranges(&[0..size]).unwrap(),
HashSet::from([0, 1]),
);
assert_eq!(fec.rounds_for_ranges(&[0..1]).unwrap(), HashSet::from([0]));
assert_eq!(
fec.rounds_for_ranges(&[4095..4096]).unwrap(),
HashSet::from([0]),
);
assert_eq!(
fec.rounds_for_ranges(&[4095..4097]).unwrap(),
HashSet::from([0, 1]),
);
assert_eq!(
fec.rounds_for_ranges(&[size - 1..size]).unwrap(),
HashSet::from([1]),
);
}
fn corrupt_byte(file: &mut SharedCursor, offset: u64) {
let mut buf = [0u8; 1];
@@ -781,7 +877,9 @@ mod tests {
corrupt_byte(&mut file, offset as u64);
}
// Verify that all the single-byte errors can be fixed.
// Verify that all the single-byte errors can be fixed. We don't test
// for Error::TooManyErrors because of the chance of false positives due
// to the nature of RS.
fec.repair(&file, &file, &fec_data, &cancel_signal).unwrap();
let repaired_digest = {
@@ -792,12 +890,17 @@ mod tests {
};
assert_eq!(repaired_digest.as_ref(), orig_digest.as_ref());
// We don't test for Error::TooManyErrors because of the chance of false
// positives due to the nature of RS.
// Intentionally update some data.
corrupt_byte(&mut file, 0);
let mut fec_data_updated = fec_data.clone();
let fec_data = fec.generate(&file, &cancel_signal).unwrap();
fec.update(&file, &[0..1], &mut fec_data_updated, &cancel_signal)
.unwrap();
assert_eq!(fec_data_updated, fec_data);
}
#[test]
fn generate_verify_repair() {
fn generate_update_verify_repair() {
for block_size in [1, 2, 4, 8, 16, 32, 64] {
for rs_k in verityrs::FN_ENCODE.keys() {
println!("Testing block_size={block_size}, rs_k={rs_k}");
+744
View File
@@ -0,0 +1,744 @@
/*
* SPDX-FileCopyrightText: 2023 Andrew Gunnerson
* SPDX-License-Identifier: GPL-3.0-only
*/
use std::{
fmt,
io::{self, Cursor, Read, SeekFrom, Write},
ops::Range,
sync::atomic::AtomicBool,
};
use bstr::ByteSlice;
use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt};
use num_traits::ToPrimitive;
use rayon::{
iter::{IndexedParallelIterator, ParallelIterator},
slice::ParallelSliceMut,
};
use ring::digest::{Algorithm, Context};
use thiserror::Error;
use crate::{
format::{avb, padding},
stream::{self, FromReader, ReadSeekReopen, ReadStringExt, ToWriter, WriteStringExt},
util::{self, NumBytes},
};
#[derive(Debug, Error)]
pub enum Error {
#[error("Hash tree should have size {expected} for input size {input}, but has size {actual}")]
InvalidHashTreeSize {
input: u64,
expected: usize,
actual: usize,
},
#[error("Expected root digest {expected}, but have {actual}")]
InvalidRootDigest { expected: String, actual: String },
#[error("Expected hash tree {expected}, but have {actual}")]
InvalidHashTree { expected: String, actual: String },
#[error("Invalid hash tree header magic: {:?}", .0.as_bstr())]
InvalidHeaderMagic([u8; 16]),
#[error("Invalid hash tree header version: {0}")]
InvalidHeaderVersion(u16),
#[error("Hashing algorithm not supported: {0:?}")]
UnsupportedHashAlgorithm(String),
#[error("{0:?} field is out of bounds")]
FieldOutOfBounds(&'static str),
#[error("I/O error")]
Io(#[from] io::Error),
}
type Result<T> = std::result::Result<T, Error>;
pub struct HashTree {
block_size: u32,
salted_context: Context,
}
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,
salted_context,
}
}
/// Compute the list of offset ranges that each level occupies in the hash
/// tree data. The items are returned with the bottom level's offsets first
/// in the list. Note that the bottom level is stored at the end of the hash
/// tree data.
pub fn compute_level_offsets(&self, image_size: u64) -> Result<Vec<Range<usize>>> {
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;
while level_size > u64::from(self.block_size) {
let blocks = level_size.div_ceil(u64::from(self.block_size));
level_size = blocks
.checked_mul(digest_size as u64)
.and_then(|s| padding::round(s, u64::from(self.block_size)))
.ok_or_else(|| Error::FieldOutOfBounds("level_size"))?;
// Depending on the chosen block size, the original file size could
// overflow a usize without the first level's size doing the same.
let level_size_usize = level_size
.to_usize()
.ok_or_else(|| Error::FieldOutOfBounds("level_size"))?;
ranges.push(0..level_size_usize);
}
// The hash tree puts the leaves at the end.
let mut offset = 0;
for range in ranges.iter_mut().rev() {
let level_size = range.end - range.start;
range.start += offset;
range.end += offset;
offset += level_size;
}
Ok(ranges)
}
/// Convert a list of ranges of byte offsets to a sorted, non-overlapping
/// list of block ranges.
fn blocks_for_ranges(&self, image_size: u64, ranges: &[Range<u64>]) -> Result<Vec<Range<u64>>> {
let ranges = util::merge_overlapping(ranges);
if let Some(last) = ranges.last() {
if last.end > image_size {
return Err(Error::FieldOutOfBounds("ranges"));
}
}
let block_size = u64::from(self.block_size);
let mut result = Vec::new();
for range in ranges {
let start_block = range.start / block_size;
let end_block = if range.end % block_size == 0 {
range.end / block_size
} else {
range.end.div_ceil(block_size)
};
result.push(start_block..end_block);
}
Ok(util::merge_overlapping(&result))
}
/// Calculate the hash tree digests for a single level of the tree. If the
/// reader's position is block-aligned and `image_size` is a multiple of the
/// block size, then this function can also be used to calculate the digests
/// for a portion of a level.
fn hash_partial_level(
&self,
mut reader: impl Read,
mut size: u64,
mut level_data: &mut [u8],
cancel_signal: &AtomicBool,
) -> io::Result<()> {
// Each digest must be a power of 2.
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 {
stream::check_cancel(cancel_signal)?;
let n = size.min(buf.len() as u64) as usize;
reader.read_exact(&mut buf[..n])?;
// For undersized blocks, we still hash the whole buffer, except
// with padding.
buf[n..].fill(0);
let mut context = self.salted_context.clone();
context.update(&buf);
// Add the digest to the tree level. Each tree node must be a power
// of two.
let digest = context.finish();
level_data[..digest.as_ref().len()].copy_from_slice(digest.as_ref());
level_data = &mut level_data[digest.as_ref().len()..];
level_data[..digest_padding].fill(0);
level_data = &mut level_data[digest_padding..];
size -= n as u64;
}
Ok(())
}
/// Hash one full level in parallel.
fn hash_one_level_parallel(
&self,
input: &(dyn ReadSeekReopen + Sync),
size: u64,
level_data: &mut [u8],
cancel_signal: &AtomicBool,
) -> io::Result<()> {
assert!(
size > self.block_size as u64,
"Images smaller than block size must use a normal hash",
);
// Parallelize in larger chunks to avoid too much seek thrashing.
let algorithm = self.salted_context.algorithm();
let digest_size = algorithm.output_len().next_power_of_two();
let multiplier = 1024u64;
level_data
.par_chunks_mut(digest_size * multiplier as usize)
.enumerate()
.map(|(chunk, out_data)| -> io::Result<()> {
let digests = out_data.len() / digest_size;
let in_start = (chunk as u64) * multiplier * u64::from(self.block_size);
let in_size = ((digests as u64) * u64::from(self.block_size)).min(size - in_start);
let mut reader = input.reopen_boxed()?;
reader.seek(SeekFrom::Start(in_start))?;
self.hash_partial_level(reader, in_size, out_data, cancel_signal)
})
.collect::<io::Result<()>>()?;
Ok(())
}
/// Update parts of the hash tree level corresponding to the specified
/// blocks.
fn hash_partial_level_parallel(
&self,
input: &(dyn ReadSeekReopen + Sync),
size: u64,
block_ranges: &[Range<u64>],
level_data: &mut [u8],
cancel_signal: &AtomicBool,
) -> io::Result<()> {
let algorithm = self.salted_context.algorithm();
let digest_size = algorithm.output_len().next_power_of_two();
level_data
.par_chunks_exact_mut(digest_size)
.enumerate()
.filter(|(chunk, _)| util::ranges_contains(block_ranges, &(*chunk as u64)))
.map(|(chunk, out_data)| -> io::Result<()> {
let in_start = (chunk as u64) * u64::from(self.block_size);
let in_size = u64::from(self.block_size).min(size - in_start);
let mut reader = input.reopen_boxed()?;
reader.seek(SeekFrom::Start(in_start))?;
self.hash_partial_level(reader, in_size, out_data, cancel_signal)
})
.collect::<io::Result<()>>()?;
Ok(())
}
/// Compute the hash tree and return the root digest. If `ranges` is
/// specified, then only the input file blocks containing those ranges are
/// recomputed.
///
/// `hash_tree_data` must match `level_offsets`. In other words, the ending
/// offset of the leaf layer of the tree must equal `hash_tree_data`'s size.
fn calculate(
&self,
input: &(dyn ReadSeekReopen + Sync),
image_size: u64,
ranges: Option<&[Range<u64>]>,
level_offsets: &[Range<usize>],
hash_tree_data: &mut [u8],
cancel_signal: &AtomicBool,
) -> Result<Vec<u8>> {
// Small files are hashed directly.
if image_size <= u64::from(self.block_size) {
let mut reader = input.reopen_boxed()?;
let mut buf = vec![0u8; image_size as usize];
reader.read_exact(&mut buf)?;
let mut context = self.salted_context.clone();
context.update(&buf);
let digest = context.finish();
return Ok(digest.as_ref().to_vec());
}
// Large files use the hash tree.
for (i, level_range) in level_offsets.iter().enumerate() {
let (front, back) = hash_tree_data.split_at_mut(level_range.end);
let level_data = &mut front[level_range.clone()];
if i > 0 {
// Hash the previous level.
let prev_range = level_offsets[i - 1].clone();
let prev_size = prev_range.end - prev_range.start;
let prev_data = &back[..prev_size];
self.hash_partial_level(
Cursor::new(prev_data),
prev_size as u64,
level_data,
cancel_signal,
)?;
} else if let Some(r) = ranges {
// Read partial blocks from file.
let block_ranges = self.blocks_for_ranges(image_size, r)?;
self.hash_partial_level_parallel(
input,
image_size,
&block_ranges,
level_data,
cancel_signal,
)?;
} else {
// Read entire file.
self.hash_one_level_parallel(input, image_size, level_data, cancel_signal)?;
}
// No need to explicitly ensure the level is padded to the block
// size since the tree is initialized with zeros.
}
// Calculate the root hash.
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();
Ok(root_hash)
}
/// Generate hash tree data for the file. Returns the root digest and the
/// hash tree data.
pub fn generate(
&self,
input: &(dyn ReadSeekReopen + Sync),
image_size: u64,
cancel_signal: &AtomicBool,
) -> Result<(Vec<u8>, Vec<u8>)> {
let offsets = self.compute_level_offsets(image_size)?;
let hash_tree_size = offsets.first().map(|r| r.end).unwrap_or(0);
let mut hash_tree_data = vec![0u8; hash_tree_size];
let root_digest = self.calculate(
input,
image_size,
None,
&offsets,
&mut hash_tree_data,
cancel_signal,
)?;
Ok((root_digest, hash_tree_data))
}
/// Update hash tree data corresponding to the specified file ranges.
/// Returns the new root digest.
pub fn update(
&self,
input: &(dyn ReadSeekReopen + Sync),
image_size: u64,
ranges: &[Range<u64>],
hash_tree_data: &mut [u8],
cancel_signal: &AtomicBool,
) -> Result<Vec<u8>> {
let offsets = self.compute_level_offsets(image_size)?;
let hash_tree_size = offsets.first().map(|r| r.end).unwrap_or(0);
if hash_tree_data.len() != hash_tree_size {
return Err(Error::InvalidHashTreeSize {
input: image_size,
expected: hash_tree_size,
actual: hash_tree_data.len(),
});
}
self.calculate(
input,
image_size,
Some(ranges),
&offsets,
hash_tree_data,
cancel_signal,
)
}
/// Verify that the file contains no errors.
pub fn verify(
&self,
input: &(dyn ReadSeekReopen + Sync),
image_size: u64,
root_digest: &[u8],
hash_tree_data: &[u8],
cancel_signal: &AtomicBool,
) -> Result<()> {
let offsets = self.compute_level_offsets(image_size)?;
let hash_tree_size = offsets.first().map(|r| r.end).unwrap_or(0);
if hash_tree_data.len() != hash_tree_size {
return Err(Error::InvalidHashTreeSize {
input: image_size,
expected: hash_tree_size,
actual: hash_tree_data.len(),
});
}
let (actual_root_digest, actual_hash_tree_data) =
self.generate(input, image_size, cancel_signal)?;
if root_digest != actual_root_digest {
return Err(Error::InvalidRootDigest {
expected: hex::encode(root_digest),
actual: hex::encode(&actual_root_digest),
});
}
if hash_tree_data != actual_hash_tree_data {
// These are multiple megabytes, so only report the hashes.
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),
actual: hex::encode(actual),
});
}
Ok(())
}
}
/// A type for reading and writing a custom hash tree image format.
///
/// File format:
/// - [0 .. 16] - ASCII - "avbroot!hashtree"
/// - [16 .. 18] - U16LE - Version
/// - [18 .. 26] - U64LE - Image size
/// - [26 .. 30] - U32LE - Block size
/// - [30 .. 46] - ASCII - Hash algorithm
/// - [46 .. 48] - U16LE - Salt size
/// - [48 .. 50] - U16LE - Root digest size
/// - [50 .. 54] - U32LE - Hash tree size
/// - [<variable>] - BINARY - Salt
/// - [<variable>] - BINARY - Root digest
/// - [<variable>] - BINARY - Hash tree
#[derive(Clone, PartialEq, Eq)]
pub struct HashTreeImage {
pub image_size: u64,
pub block_size: u32,
pub algorithm: String,
pub salt: Vec<u8>,
pub root_digest: Vec<u8>,
pub hash_tree: Vec<u8>,
}
impl fmt::Debug for HashTreeImage {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("HashTreeImage")
.field("block_size", &self.block_size)
.field("algorithm", &self.algorithm)
.field("salt", &hex::encode(&self.salt))
.field("root_digest", &hex::encode(&self.root_digest))
.field("hash_tree", &NumBytes(self.hash_tree.len()))
.finish()
}
}
impl HashTreeImage {
const MAGIC: &'static [u8; 16] = b"avbroot!hashtree";
const VERSION: u16 = 1;
pub fn ring_algorithm(name: &str) -> Result<&'static Algorithm> {
avb::ring_algorithm(name, false)
.map_err(|_| Error::UnsupportedHashAlgorithm(name.to_owned()))
}
/// Generate hash tree data for a file.
pub fn generate(
input: &(dyn ReadSeekReopen + Sync),
block_size: u32,
algorithm: &str,
salt: &[u8],
cancel_signal: &AtomicBool,
) -> Result<Self> {
let image_size = {
let mut file = input.reopen_boxed()?;
file.seek(SeekFrom::End(0))?
};
let ring_algorithm = Self::ring_algorithm(algorithm)?;
let hash_tree = HashTree::new(block_size, ring_algorithm, salt);
let (root_digest, hash_tree_data) = hash_tree.generate(input, image_size, cancel_signal)?;
Ok(Self {
image_size,
block_size,
algorithm: algorithm.to_owned(),
salt: salt.to_vec(),
root_digest,
hash_tree: hash_tree_data,
})
}
/// Update hash tree data coreesponding to the specified file ranges.
pub fn update(
&mut self,
input: &(dyn ReadSeekReopen + Sync),
ranges: &[Range<u64>],
cancel_signal: &AtomicBool,
) -> Result<()> {
let ring_algorithm = Self::ring_algorithm(&self.algorithm)?;
let hash_tree = HashTree::new(self.block_size, ring_algorithm, &self.salt);
self.root_digest = hash_tree.update(
input,
self.image_size,
ranges,
&mut self.hash_tree,
cancel_signal,
)?;
Ok(())
}
/// Check that a file contains no errors.
pub fn verify(
&self,
input: &(dyn ReadSeekReopen + Sync),
cancel_signal: &AtomicBool,
) -> Result<()> {
let ring_algorithm = Self::ring_algorithm(&self.algorithm)?;
let hash_tree = HashTree::new(self.block_size, ring_algorithm, &self.salt);
hash_tree.verify(
input,
self.image_size,
&self.root_digest,
&self.hash_tree,
cancel_signal,
)
}
}
impl<R: Read> FromReader<R> for HashTreeImage {
type Error = Error;
fn from_reader(mut reader: R) -> Result<Self> {
let mut magic = [0u8; 16];
reader.read_exact(&mut magic)?;
if magic != *Self::MAGIC {
return Err(Error::InvalidHeaderMagic(magic));
}
let version = reader.read_u16::<LittleEndian>()?;
if version != Self::VERSION {
return Err(Error::InvalidHeaderVersion(version));
}
let image_size = reader.read_u64::<LittleEndian>()?;
let block_size = reader.read_u32::<LittleEndian>()?;
let algorithm = reader.read_string_padded(16)?;
let salt_size = reader.read_u16::<LittleEndian>()?;
let root_digest_size = reader.read_u16::<LittleEndian>()?;
let hash_tree_size = reader
.read_u32::<LittleEndian>()?
.to_usize()
.ok_or_else(|| Error::FieldOutOfBounds("hash_tree_size"))?;
let mut salt = vec![0u8; usize::from(salt_size)];
reader.read_exact(&mut salt)?;
let mut root_digest = vec![0u8; usize::from(root_digest_size)];
reader.read_exact(&mut root_digest)?;
let mut hash_tree = vec![0u8; hash_tree_size];
reader.read_exact(&mut hash_tree)?;
Ok(Self {
image_size,
block_size,
algorithm,
salt,
root_digest,
hash_tree,
})
}
}
impl<W: Write> ToWriter<W> for HashTreeImage {
type Error = Error;
fn to_writer(&self, mut writer: W) -> Result<()> {
let salt_size = self
.salt
.len()
.to_u16()
.ok_or_else(|| Error::FieldOutOfBounds("salt_size"))?;
let root_digest_size = self
.root_digest
.len()
.to_u16()
.ok_or_else(|| Error::FieldOutOfBounds("root_digest_size"))?;
let hash_tree_size = self
.hash_tree
.len()
.to_u32()
.ok_or_else(|| Error::FieldOutOfBounds("hash_tree_size"))?;
writer.write_all(Self::MAGIC)?;
writer.write_u16::<LittleEndian>(Self::VERSION)?;
writer.write_u64::<LittleEndian>(self.image_size)?;
writer.write_u32::<LittleEndian>(self.block_size)?;
writer.write_string_padded(&self.algorithm, 16)?;
writer.write_u16::<LittleEndian>(salt_size)?;
writer.write_u16::<LittleEndian>(root_digest_size)?;
writer.write_u32::<LittleEndian>(hash_tree_size)?;
writer.write_all(&self.salt)?;
writer.write_all(&self.root_digest)?;
writer.write_all(&self.hash_tree)?;
Ok(())
}
}
#[cfg(test)]
mod tests {
use std::io::{Seek, Write};
use assert_matches::assert_matches;
use crate::stream::SharedCursor;
use super::*;
#[test]
fn calculate_level_ranges() {
let hash_tree = HashTree::new(4096, &ring::digest::SHA256, &[]);
assert_eq!(
hash_tree.compute_level_offsets(0).unwrap(),
&[] as &[Range<usize>],
);
assert_eq!(
hash_tree.compute_level_offsets(1024 * 1024 * 1024).unwrap(),
&[69632..8458240, 4096..69632, 0..4096],
)
}
#[test]
fn blocks_for_ranges() {
let hash_tree = HashTree::new(4096, &ring::digest::SHA256, b"Salt");
assert_eq!(
hash_tree.blocks_for_ranges(16384, &[0..16384]).unwrap(),
&[0..4],
);
assert_eq!(hash_tree.blocks_for_ranges(16384, &[0..0]).unwrap(), &[]);
assert_eq!(
hash_tree
.blocks_for_ranges(16384, &[12287..12289, 0..1, 5000..5001])
.unwrap(),
&[0..4],
);
assert_matches!(
hash_tree.blocks_for_ranges(16384, &[0..16385]),
Err(Error::FieldOutOfBounds(_))
);
}
#[test]
fn generate_update_verify() {
let cancel_signal = AtomicBool::new(false);
let hash_tree = HashTree::new(64, &ring::digest::SHA256, b"Salt");
let mut input = SharedCursor::new();
// Try input smaller than one block.
let (root_digest, hash_tree_data) = hash_tree.generate(&input, 0, &cancel_signal).unwrap();
assert_eq!(
root_digest,
&[
0x15, 0x0f, 0xe5, 0x51, 0x40, 0x30, 0xb1, 0x43, 0x4a, 0x5d, 0xea, 0xf4, 0x91, 0xec,
0xe9, 0x2c, 0x0e, 0x64, 0x97, 0x44, 0x7d, 0x6d, 0xe7, 0xbd, 0x6b, 0xa8, 0x5e, 0x8c,
0xae, 0x1e, 0x00, 0xa3
],
);
assert_eq!(hash_tree_data, &[]);
// Try larger input that spans multiple blocks are results in an actual
// hash tree being created.
input.write_all(&b"Data".repeat(25)).unwrap();
let (root_digest, mut hash_tree_data) =
hash_tree.generate(&input, 100, &cancel_signal).unwrap();
assert_eq!(
root_digest,
&[
0x92, 0xc3, 0xd7, 0x4a, 0x64, 0x03, 0x4b, 0xcc, 0xa9, 0x9a, 0x44, 0xf6, 0x81, 0xa2,
0x4d, 0xdd, 0x97, 0xd3, 0xda, 0x84, 0xdc, 0xe2, 0x1b, 0x83, 0xd1, 0x7b, 0xab, 0x60,
0x59, 0xe8, 0x45, 0x59
],
);
assert_eq!(
hash_tree_data,
&[
0x7e, 0x33, 0x47, 0xb6, 0xf3, 0x7c, 0xde, 0x0e, 0xe2, 0x8d, 0x9e, 0x49, 0x8e, 0xd4,
0xbd, 0x53, 0x3a, 0xa1, 0xff, 0xeb, 0x4f, 0x6d, 0x5a, 0x5f, 0x55, 0x28, 0x37, 0x79,
0xd0, 0x25, 0x07, 0xd5, 0xb7, 0x7f, 0x1a, 0x48, 0x92, 0x12, 0x91, 0xdb, 0x92, 0x04,
0x74, 0xf6, 0x86, 0x31, 0xfc, 0x64, 0xb6, 0xc8, 0x72, 0xb0, 0xf7, 0x7d, 0x24, 0xa4,
0x3c, 0x87, 0x1f, 0xc9, 0xd8, 0x17, 0x8a, 0xd9
],
);
// Change some data and update the hash tree.
input.rewind().unwrap();
input.write_all(b"Changed").unwrap();
let root_digest = hash_tree
.update(&input, 100, &[0..7], &mut hash_tree_data, &cancel_signal)
.unwrap();
assert_eq!(
root_digest,
&[
0x8d, 0x03, 0xad, 0x18, 0xf2, 0x53, 0x13, 0x59, 0xf5, 0xbf, 0x68, 0x0e, 0x0c, 0x4a,
0x86, 0xe2, 0x6e, 0xaa, 0x3d, 0x4b, 0x0f, 0x1b, 0x57, 0xad, 0x92, 0xe7, 0xbf, 0x3e,
0xa6, 0xb1, 0x2e, 0xcc
],
);
assert_eq!(
hash_tree_data,
&[
0xfe, 0x46, 0xf7, 0x8c, 0xa1, 0xd9, 0xc8, 0xdd, 0x47, 0x9e, 0x6c, 0x32, 0x7c, 0x38,
0x7f, 0x09, 0xe1, 0x58, 0x92, 0xa3, 0xb6, 0xbd, 0x96, 0xef, 0x10, 0xe8, 0x30, 0xb0,
0x37, 0x8d, 0xef, 0x9a, 0xb7, 0x7f, 0x1a, 0x48, 0x92, 0x12, 0x91, 0xdb, 0x92, 0x04,
0x74, 0xf6, 0x86, 0x31, 0xfc, 0x64, 0xb6, 0xc8, 0x72, 0xb0, 0xf7, 0x7d, 0x24, 0xa4,
0x3c, 0x87, 0x1f, 0xc9, 0xd8, 0x17, 0x8a, 0xd9
],
);
// Updated hash tree should match newly generated tree.
let (new_root_digest, new_hash_tree_data) =
hash_tree.generate(&input, 100, &cancel_signal).unwrap();
assert_eq!(new_root_digest, root_digest);
assert_eq!(new_hash_tree_data, hash_tree_data);
// Data should validate successfully.
hash_tree
.verify(&input, 100, &root_digest, &hash_tree_data, &cancel_signal)
.unwrap();
// But not if the data is corrupted.
input.rewind().unwrap();
input.write_all(b"Bad").unwrap();
hash_tree
.verify(&input, 100, &root_digest, &hash_tree_data, &cancel_signal)
.unwrap_err();
}
}
+1
View File
@@ -8,6 +8,7 @@ pub mod bootimage;
pub mod compression;
pub mod cpio;
pub mod fec;
pub mod hashtree;
pub mod ota;
pub mod padding;
pub mod payload;
+12 -17
View File
@@ -1,5 +1,5 @@
/*
* SPDX-FileCopyrightText: 2022-2023 Andrew Gunnerson
* SPDX-FileCopyrightText: 2022-2024 Andrew Gunnerson
* SPDX-License-Identifier: GPL-3.0-only
*/
@@ -15,15 +15,12 @@ use const_oid::{db::rfc5912, ObjectIdentifier};
use memchr::memmem;
use prost::Message;
use ring::digest::Context;
use rsa::{Pkcs1v15Sign, RsaPrivateKey};
use sha1::Sha1;
use sha2::Sha256;
use thiserror::Error;
use x509_cert::{der::Encode, Certificate};
use zip::{result::ZipError, write::FileOptions, CompressionMethod, ZipArchive, ZipWriter};
use crate::{
crypto,
crypto::{self, RsaPublicKeyExt, RsaSigningKey, SignatureAlgorithm},
format::payload::{self, PayloadHeader},
protobuf::build::tools::releasetools::{ota_metadata::OtaType, OtaMetadata},
stream::{self, FromReader, HashingReader, HashingWriter},
@@ -40,7 +37,7 @@ const NAME_PAYLOAD_METADATA: &str = "payload_metadata.bin";
pub const PF_NAME: &str = "ota-property-files";
pub const PF_STREAMING_NAME: &str = "ota-streaming-property-files";
const ZIP_EOCD_MAGIC: &[u8; 4] = b"PK\x05\x06";
pub const ZIP_EOCD_MAGIC: &[u8; 4] = b"PK\x05\x06";
const COMMENT_MESSAGE: &[u8] = b"signed by avbroot\0";
@@ -88,8 +85,6 @@ pub enum Error {
Spki(#[from] pkcs8::spki::Error),
#[error("x509 DER error")]
Der(#[from] x509_cert::der::Error),
#[error("RSA error")]
Rsa(#[from] rsa::Error),
#[error("Zip error")]
Zip(#[from] ZipError),
#[error("I/O error")]
@@ -152,15 +147,15 @@ pub fn parse_legacy_metadata(data: &str) -> Result<OtaMetadata> {
}
"post-build-incremental" => {
let p = metadata.postcondition.get_or_insert_with(Default::default);
p.build_incremental = value.to_owned();
value.clone_into(&mut p.build_incremental);
}
"post-sdk-level" => {
let p = metadata.postcondition.get_or_insert_with(Default::default);
p.sdk_level = value.to_owned();
value.clone_into(&mut p.sdk_level);
}
"post-security-patch-level" => {
let p = metadata.postcondition.get_or_insert_with(Default::default);
p.security_patch_level = value.to_owned();
value.clone_into(&mut p.security_patch_level);
}
"post-timestamp" => {
let p = metadata.postcondition.get_or_insert_with(Default::default);
@@ -176,7 +171,7 @@ pub fn parse_legacy_metadata(data: &str) -> Result<OtaMetadata> {
}
"pre-build-incremental" => {
let p = metadata.precondition.get_or_insert_with(Default::default);
p.build_incremental = value.to_owned();
value.clone_into(&mut p.build_incremental);
}
"spl-downgrade" => metadata.spl_downgrade = parse_yes()?,
k if k.ends_with("-property-files") => {
@@ -586,12 +581,12 @@ pub fn verify_ota(mut reader: impl Read + Seek, cancel_signal: &AtomicBool) -> R
reader.seek(SeekFrom::Start(0))?;
// We support SHA1 for verification only.
let (algorithm, scheme) = if signer.digest_alg.oid == rfc5912::ID_SHA_256 {
(&ring::digest::SHA256, Pkcs1v15Sign::new::<Sha256>())
let (algorithm, algo) = if signer.digest_alg.oid == rfc5912::ID_SHA_256 {
(&ring::digest::SHA256, SignatureAlgorithm::Sha256WithRsa)
} else {
(
&ring::digest::SHA1_FOR_LEGACY_USE_ONLY,
Pkcs1v15Sign::new::<Sha1>(),
SignatureAlgorithm::Sha1WithRsa,
)
};
@@ -603,7 +598,7 @@ pub fn verify_ota(mut reader: impl Read + Seek, cancel_signal: &AtomicBool) -> R
let digest = context.finish();
// Verify the signature against the public key.
public_key.verify(scheme, digest.as_ref(), signer.signature.as_bytes())?;
public_key.verify_sig(algo, digest.as_ref(), signer.signature.as_bytes())?;
Ok(cert.clone())
}
@@ -661,7 +656,7 @@ impl<W: Write> SigningWriter<W> {
}
}
pub fn finish(mut self, key: &RsaPrivateKey, cert: &Certificate) -> Result<W> {
pub fn finish(mut self, key: &RsaSigningKey, cert: &Certificate) -> Result<W> {
if self.used < self.queue.len() {
return Err(
io::Error::new(io::ErrorKind::InvalidData, "Too small to contain EOCD").into(),
+368 -132
View File
@@ -1,11 +1,12 @@
/*
* SPDX-FileCopyrightText: 2022-2023 Andrew Gunnerson
* SPDX-FileCopyrightText: 2022-2024 Andrew Gunnerson
* SPDX-License-Identifier: GPL-3.0-only
*/
use std::{
collections::{HashMap, HashSet},
io::{self, Cursor, Read, Seek, SeekFrom, Write},
ops::Range,
sync::atomic::AtomicBool,
};
@@ -13,30 +14,33 @@ use base64::engine::general_purpose::STANDARD;
use base64::Engine;
use byteorder::{BigEndian, ReadBytesExt};
use bzip2::write::BzDecoder;
use num_traits::ToPrimitive;
use prost::Message;
use rayon::prelude::{IntoParallelIterator, IntoParallelRefIterator, ParallelIterator};
use ring::digest::{Context, Digest};
use rsa::{traits::PublicKeyParts, Pkcs1v15Sign, RsaPrivateKey};
use sha2::Sha256;
use thiserror::Error;
use x509_cert::Certificate;
use xz2::{
use liblzma::{
stream::{Check, Stream},
write::XzDecoder,
write::XzEncoder,
};
use num_traits::ToPrimitive;
use prost::Message;
use rayon::{
iter::{IndexedParallelIterator, IntoParallelRefMutIterator},
prelude::{IntoParallelIterator, IntoParallelRefIterator, ParallelIterator},
};
use ring::digest::{Context, Digest};
use serde::{Deserialize, Serialize};
use thiserror::Error;
use x509_cert::Certificate;
use crate::{
crypto,
crypto::{self, RsaPublicKeyExt, RsaSigningKey, SignatureAlgorithm},
protobuf::chromeos_update_engine::{
install_operation::Type, signatures::Signature, DeltaArchiveManifest, Extent,
InstallOperation, PartitionInfo, PartitionUpdate, Signatures,
},
stream::{
self, CountingReader, CountingWriter, FromReader, HashingWriter, ReadDiscardExt,
ReadSeekReopen, Reopen, SharedCursor, WriteSeek,
self, CountingReader, FromReader, HashingWriter, ReadDiscardExt, ReadSeekReopen, WriteSeek,
WriteSeekReopen,
},
util,
};
const OTA_MAGIC: &[u8; 4] = b"CrAU";
@@ -79,6 +83,8 @@ pub enum Error {
size: u64,
block_size: u32,
},
#[error("Destination extents are not in order")]
ExtentsNotInOrder,
#[error("Partition not found in payload: {0}")]
MissingPartition(String),
#[error("Partitions not found in payload: {0:?}")]
@@ -92,20 +98,20 @@ pub enum Error {
#[error("Failed to decode protobuf message")]
ProtobufDecode(#[from] prost::DecodeError),
#[error("XZ stream error")]
XzStream(#[from] xz2::stream::Error),
#[error("RSA error")]
Rsa(#[from] rsa::Error),
XzStream(#[from] liblzma::stream::Error),
#[error("I/O error")]
Io(#[from] io::Error),
}
type Result<T> = std::result::Result<T, Error>;
#[derive(Clone, Debug)]
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct PayloadHeader {
pub version: u64,
pub manifest: DeltaArchiveManifest,
#[serde(skip)]
pub metadata_signature_size: u32,
#[serde(skip)]
pub blob_offset: u64,
}
@@ -168,9 +174,8 @@ impl<R: Read> FromReader<R> for PayloadHeader {
/// Sign `digest` with `key` and return a [`Signatures`] protobuf struct with
/// the signature padded to the maximum size.
fn sign_digest(digest: &[u8], key: &RsaPrivateKey) -> Result<Signatures> {
let scheme = Pkcs1v15Sign::new::<Sha256>();
let mut digest_signed = key.sign(scheme, digest)?;
fn sign_digest(digest: &[u8], key: &RsaSigningKey) -> Result<Signatures> {
let mut digest_signed = key.sign(SignatureAlgorithm::Sha256WithRsa, digest)?;
assert!(
digest_signed.len() <= key.size(),
"Signature exceeds maximum size",
@@ -207,8 +212,7 @@ fn verify_digest(digest: &[u8], signatures: &Signatures, cert: &Certificate) ->
};
let without_padding = &data[..size as usize];
let scheme = Pkcs1v15Sign::new::<Sha256>();
match public_key.verify(scheme, digest, without_padding) {
match public_key.verify_sig(SignatureAlgorithm::Sha256WithRsa, digest, without_padding) {
Ok(_) => return Ok(()),
Err(e) => last_error = Some(e),
}
@@ -285,7 +289,7 @@ pub struct PayloadWriter<W: Write> {
h_partial: Context,
/// Includes signatures (hashes are for properties file).
h_full: Context,
key: RsaPrivateKey,
key: RsaSigningKey,
}
/// Write data to a writer and one or more hashers.
@@ -308,7 +312,7 @@ impl<W: Write> PayloadWriter<W> {
/// fields are ignored and internally recomputed to guarantee that there are
/// no gaps. All partitions' install operation data is written to the blob
/// section in order.
pub fn new(mut inner: W, mut header: PayloadHeader, key: RsaPrivateKey) -> Result<Self> {
pub fn new(mut inner: W, mut header: PayloadHeader, key: RsaSigningKey) -> Result<Self> {
let mut blob_size = 0;
// The blob must contain all data in sequential order with no gaps.
@@ -383,12 +387,12 @@ impl<W: Write> PayloadWriter<W> {
}
/// Finalize the payload. If this function is not called, the payload will
/// be left in an incomplete state. Returns the original writer, the
/// contents that should be written for `payload_properties.txt` and the
/// length of the header + manifest + manifest signature sections (for
/// constructing the `payload_metadata.bin` OTA metadata property files
/// be left in an incomplete state. Returns the original writer, the final
/// header, the contents that should be written for `payload_properties.txt`
/// and the length of the header + manifest + manifest signature sections
/// (for constructing the `payload_metadata.bin` OTA metadata property files
/// entry).
pub fn finish(mut self) -> Result<(W, String, u64)> {
pub fn finish(mut self) -> Result<(W, PayloadHeader, String, u64)> {
// Append payload signature.
let payload_partial_hash = self.h_partial.clone().finish();
let payload_sig = sign_digest(payload_partial_hash.as_ref(), &self.key)?;
@@ -412,7 +416,7 @@ impl<W: Write> PayloadWriter<W> {
self.metadata_size as u64,
);
Ok((self.inner, properties, metadata_with_sig_size))
Ok((self.inner, self.header, properties, metadata_with_sig_size))
}
/// Prepare for writing the next source data blob corresponding to an
@@ -535,98 +539,6 @@ impl<W: Write> Write for PayloadWriter<W> {
}
}
/// A writer to produce a compressed partition image suitable for directly
/// inserting into a `payload.bin`'s blob section. The data is XZ-compressed at
/// a low compression level, primarily to collapse zeros, and the
/// [`PartitionUpdate`] instance is updated with the final size and hash. The
/// entire data will be represented as a single [`InstallOperation`] and
/// [`InstallOperation::data_offset`] will be set to `None`.
pub struct CompressedPartitionWriter<W: Write> {
inner: XzEncoder<HashingWriter<CountingWriter<W>>>,
block_size: u32,
h_uncompressed: Context,
written: u64,
}
impl<W: Write> CompressedPartitionWriter<W> {
pub fn new(writer: W, block_size: u32) -> Result<Self> {
let counting_writer = CountingWriter::new(writer);
let hashing_writer =
HashingWriter::new(counting_writer, Context::new(&ring::digest::SHA256));
// AOSP's payload_consumer does not support CRC during decompression.
// Also, we intentionally pick the lowest compression level since we
// primarily care about squishing zeros. The non-zero portions of boot
// images are usually already-compressed kernels and ramdisks.
let stream = Stream::new_easy_encoder(0, Check::None)?;
let xz_writer = XzEncoder::new_stream(hashing_writer, stream);
Ok(Self {
inner: xz_writer,
block_size,
h_uncompressed: Context::new(&ring::digest::SHA256),
written: 0,
})
}
/// Finish writing and update the [`PartitionUpdate`] instance with the new
/// size, hash, and install operation metadata.
pub fn finish(mut self, partition: &mut PartitionUpdate) -> Result<W> {
if self.written % u64::from(self.block_size) != 0 {
return Err(Error::InvalidPartitionSize {
name: partition.partition_name.clone(),
size: self.written,
block_size: self.block_size,
});
}
self.inner.flush()?;
let hashing_writer = self.inner.finish()?;
let digest_uncompressed = self.h_uncompressed.finish();
let (counting_writer, context_compressed) = hashing_writer.finish();
let digest_compressed = context_compressed.finish();
// XzEncoder::total_out() cannot be used for an exact byte count because
// XzEncoder::finish() writes data.
let (writer, size_compressed) = counting_writer.finish();
partition.new_partition_info = Some(PartitionInfo {
size: Some(self.written),
hash: Some(digest_uncompressed.as_ref().to_vec()),
});
let extent = Extent {
start_block: Some(0),
num_blocks: Some(self.written / u64::from(self.block_size)),
};
// data_offset must be manually updated by the caller.
let mut operation = InstallOperation::default();
operation.set_type(Type::ReplaceXz);
operation.data_length = Some(size_compressed);
operation.dst_extents.push(extent);
operation.data_sha256_hash = Some(digest_compressed.as_ref().to_vec());
partition.operations.clear();
partition.operations.push(operation);
Ok(writer)
}
}
impl<W: Write> Write for CompressedPartitionWriter<W> {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
let n = self.inner.write(buf)?;
self.h_uncompressed.update(&buf[..n]);
self.written += n as u64;
Ok(n)
}
fn flush(&mut self) -> io::Result<()> {
self.inner.flush()
}
}
/// Verify the payload signatures using the specified certificate and check that
/// the digests in `payload_properties.txt` are correct.
pub fn verify_payload(
@@ -867,29 +779,29 @@ pub fn apply_operation(
Ok(())
}
/// Extract the specified image from the payload into memory. This is done
/// multithreaded and uses rayon's global thread pool. `open_payload` will be
/// called from multiple threads.
pub fn extract_image_to_memory(
/// Extract the specified image from the payload. This is done multithreaded and
/// uses rayon's global thread pool. Both the `payload` and `output` streams
/// will be reopened from multiple threads.
pub fn extract_image(
payload: &(dyn ReadSeekReopen + Sync),
output: &(dyn WriteSeekReopen + Sync),
header: &PayloadHeader,
partition_name: &str,
cancel_signal: &AtomicBool,
) -> Result<SharedCursor> {
) -> Result<()> {
let partition = header
.manifest
.partitions
.iter()
.find(|p| p.partition_name == partition_name)
.ok_or_else(|| Error::MissingPartition(partition_name.to_owned()))?;
let stream = SharedCursor::default();
partition
.operations
.par_iter()
.map(|op| -> Result<()> {
let reader = payload.reopen_boxed()?;
let writer = stream.reopen()?;
let writer = output.reopen_boxed()?;
apply_operation(
reader,
@@ -902,9 +814,7 @@ pub fn extract_image_to_memory(
Ok(())
})
.collect::<Result<_>>()?;
Ok(stream)
.collect::<Result<_>>()
}
/// Extract the specified partition images from the payload into writers. This
@@ -954,3 +864,329 @@ pub fn extract_images<'a>(
})
.collect()
}
fn compress_chunk(raw_data: &[u8], cancel_signal: &AtomicBool) -> Result<(Vec<u8>, Digest)> {
let reader = Cursor::new(raw_data);
let writer = Cursor::new(Vec::new());
let hashing_writer = HashingWriter::new(writer, Context::new(&ring::digest::SHA256));
// AOSP's payload_consumer does not support checking CRC during
// decompression. Also, we intentionally pick the lowest compression level
// since we primarily care about squishing zeros. The non-zero portions of
// boot images are usually already-compressed kernels and ramdisks.
let stream = Stream::new_easy_encoder(0, Check::None)?;
let mut xz_writer = XzEncoder::new_stream(hashing_writer, stream);
stream::copy_n(reader, &mut xz_writer, raw_data.len() as u64, cancel_signal)?;
let hashing_writer = xz_writer.finish()?;
let (writer, context_compressed) = hashing_writer.finish();
let digest_compressed = context_compressed.finish();
let data = writer.into_inner();
Ok((data, digest_compressed))
}
fn compress_cow_size(mut raw_data: &[u8], block_size: u32) -> u64 {
let mut total = 0;
while !raw_data.is_empty() {
let n = raw_data.len().min(block_size as usize);
let compressed = lz4_flex::block::compress(&raw_data[..n]);
total += compressed.len().min(n) as u64;
raw_data = &raw_data[n..];
}
total
}
/// Compress the image and return the corresponding information to insert into
/// the payload manifest's [`PartitionUpdate`] instance. The uncompressed data
/// is split into 2 MiB chunks, which are read and compressed in parallel, and
/// then written in parallel (but in order) to the output. Each chunk will have
/// a corresponding [`InstallOperation`] in the return value. The caller must
/// update [`InstallOperation::data_offset`] in each operation manually because
/// the initial values are relative to 0.
///
/// If `need_cow_estimate` is true, the VABC CoW v2 + lz4 size estimate will be
/// computed. The caller must update [`PartitionUpdate::estimate_cow_size`] with
/// this value or else update_engine may fail to flash the partition due to
/// running out of space on the CoW block device. CoW v2 + other algorithms and
/// also CoW v3 are currently unsupported because there currently are no known
/// OTAs that use those configurations.
pub fn compress_image(
input: &(dyn ReadSeekReopen + Sync),
output: &(dyn WriteSeekReopen + Sync),
partition_name: &str,
block_size: u32,
need_cow_estimate: bool,
cancel_signal: &AtomicBool,
) -> Result<(PartitionInfo, Vec<InstallOperation>, Option<u64>)> {
const CHUNK_SIZE: u64 = 2 * 1024 * 1024;
const CHUNK_GROUP: u64 = 32;
let file_size = input.reopen_boxed()?.seek(SeekFrom::End(0))?;
let final_chunk_different = file_size % CHUNK_SIZE != 0;
if file_size % u64::from(block_size) != 0 || CHUNK_SIZE % u64::from(block_size) != 0 {
return Err(Error::InvalidPartitionSize {
name: partition_name.to_owned(),
size: file_size,
block_size,
});
}
let chunks_total = file_size.div_ceil(CHUNK_SIZE);
let mut bytes_compressed = 0;
let mut context_uncompressed = Context::new(&ring::digest::SHA256);
let mut cow_estimate = 0;
let mut operations = vec![];
// Read the file one group at a time. This allows for some parallelization
// without reading the entire file into memory. This is necessary because we
// need to compute the checksum of the entire file.
while (operations.len() as u64) < chunks_total {
let chunks_done = operations.len() as u64;
let chunks_group = (chunks_total - chunks_done).min(CHUNK_GROUP);
let uncompressed_data_group = (chunks_done..chunks_done + chunks_group)
.into_par_iter()
.map(|chunk| -> Result<(u64, Vec<u8>)> {
let mut reader = input.reopen_boxed()?;
let offset = reader.seek(SeekFrom::Start(chunk * CHUNK_SIZE))?;
let chunk_size = if final_chunk_different && chunk == chunks_total - 1 {
file_size % CHUNK_SIZE
} else {
CHUNK_SIZE
};
let mut data = vec![0u8; chunk_size as usize];
stream::check_cancel(cancel_signal)?;
reader.read_exact(&mut data)?;
Ok((offset, data))
})
.collect::<Result<Vec<_>>>()?;
for (_, data) in &uncompressed_data_group {
context_uncompressed.update(data);
}
let mut compressed_data_group = uncompressed_data_group
.into_par_iter()
.map(
|(raw_offset, raw_data)| -> Result<(Vec<u8>, InstallOperation, u64)> {
let (data, digest_compressed) = compress_chunk(&raw_data, cancel_signal)?;
let cow_size = if need_cow_estimate {
compress_cow_size(&raw_data, block_size)
} else {
0
};
let extent = Extent {
start_block: Some(raw_offset / u64::from(block_size)),
num_blocks: Some(raw_data.len() as u64 / u64::from(block_size)),
};
let mut operation = InstallOperation::default();
operation.set_type(Type::ReplaceXz);
operation.data_length = Some(data.len() as u64);
operation.dst_extents.push(extent);
operation.data_sha256_hash = Some(digest_compressed.as_ref().to_vec());
Ok((data, operation, cow_size))
},
)
.collect::<Result<Vec<_>>>()?;
for (data, operation, cow_size) in &mut compressed_data_group {
operation.data_offset = Some(bytes_compressed);
bytes_compressed += data.len() as u64;
cow_estimate += *cow_size;
}
let group_operations = compressed_data_group
.into_par_iter()
.map(|(data, operation, _)| -> Result<InstallOperation> {
let mut writer = output.reopen_boxed()?;
writer.seek(SeekFrom::Start(operation.data_offset.unwrap()))?;
writer.write_all(&data)?;
Ok(operation)
})
.collect::<Result<Vec<_>>>()?;
operations.extend(group_operations.into_iter());
}
let digest_uncompressed = context_uncompressed.finish();
let partition_info = PartitionInfo {
size: Some(file_size),
hash: Some(digest_uncompressed.as_ref().to_vec()),
};
let cow_estimate = if need_cow_estimate {
// Because lz4_flex compresses better than official lz4.
let fudge = cow_estimate / 100;
Some(cow_estimate + fudge)
} else {
None
};
Ok((partition_info, operations, cow_estimate))
}
fn extents_sorted(operations: &[InstallOperation]) -> bool {
let mut offset = 0;
for operation in operations {
if operation.dst_extents.is_empty() {
return false;
}
for extent in &operation.dst_extents {
let Some(start) = extent.start_block else {
return false;
};
let Some(size) = extent.num_blocks else {
return false;
};
if start != offset {
return false;
}
let Some(next) = start.checked_add(size) else {
return false;
};
offset = next;
}
}
true
}
/// Compress the modified image and update the specified [`PartitionInfo`] and
/// list of [`InstallOperation`]s. [`InstallOperation`]s that do not match any
/// byte range in `ranges` will not be compressed. The caller must update
/// [`InstallOperation::data_offset`] in each operation manually because the
/// initial values are relative to 0.
///
/// Returns the ranges of indices of `operations` that were updated.
pub fn compress_modified_image(
input: &(dyn ReadSeekReopen + Sync),
output: &(dyn WriteSeekReopen + Sync),
block_size: u32,
partition_info: &mut PartitionInfo,
operations: &mut [InstallOperation],
ranges: &[Range<u64>],
cancel_signal: &AtomicBool,
) -> Result<Vec<Range<usize>>> {
const OPERATION_GROUP: usize = 32;
// Full OTAs created by payload_generator have one extent per operation and
// they're all in order with no gaps. Verify this so we can take advantage
// of this layout.
if !extents_sorted(operations) {
return Err(Error::ExtentsNotInOrder);
}
let groups_total = operations.len().div_ceil(OPERATION_GROUP);
let mut bytes_compressed = 0;
let mut context_uncompressed = Context::new(&ring::digest::SHA256);
let mut modified_operations = vec![];
// Read the file one group at a time. This allows for some parallelization
// without reading the entire file into memory. This is necessary because we
// need to compute the checksum of the entire file.
for group in 0..groups_total {
let operation_start = group * OPERATION_GROUP;
let operation_size = (operations.len() - operation_start).min(OPERATION_GROUP);
let operation_end = operation_start + operation_size;
let uncompressed_data_group = operations[operation_start..operation_end]
.par_iter()
.map(|operation| -> Result<(Vec<u8>, bool)> {
let extents_start = operation.dst_extents[0]
.start_block()
.checked_mul(u64::from(block_size))
.ok_or_else(|| Error::FieldOutOfBounds("extents_start"))?;
let extents_size = operation
.dst_extents
.iter()
.map(|e| e.num_blocks())
.try_fold(0u64, |acc, n| acc.checked_add(n))
.and_then(|n| n.checked_mul(u64::from(block_size)))
.ok_or_else(|| Error::FieldOutOfBounds("extents_size"))?;
let extents_end = extents_start
.checked_add(extents_size)
.ok_or_else(|| Error::FieldOutOfBounds("extents_end"))?;
let extents_size = extents_size
.to_usize()
.ok_or_else(|| Error::FieldOutOfBounds("extents_size"))?;
let mut reader = input.reopen_boxed()?;
reader.seek(SeekFrom::Start(extents_start))?;
let mut data = vec![0u8; extents_size];
stream::check_cancel(cancel_signal)?;
reader.read_exact(&mut data)?;
let was_modified = util::ranges_overlaps(ranges, &(extents_start..extents_end));
Ok((data, was_modified))
})
.collect::<Result<Vec<_>>>()?;
for (data, _) in &uncompressed_data_group {
context_uncompressed.update(data);
}
// Only compress the modified chunks.
let mut compressed_data_group = operations[operation_start..operation_end]
.par_iter_mut()
.enumerate()
.zip(uncompressed_data_group)
.filter(|(_, (_, was_modified))| *was_modified)
.map(
|((i_rel, operation), (raw_data, _))| -> Result<(Vec<u8>, usize, &mut InstallOperation)> {
let (data, digest_compressed) = compress_chunk(&raw_data, cancel_signal)?;
operation.set_type(Type::ReplaceXz);
operation.data_length = Some(data.len() as u64);
operation.data_sha256_hash = Some(digest_compressed.as_ref().to_vec());
Ok((data, i_rel + operation_start, operation))
},
)
.collect::<Result<Vec<_>>>()?;
for (data, _, operation) in &mut compressed_data_group {
operation.data_offset = Some(bytes_compressed);
bytes_compressed += data.len() as u64;
}
let modified_group_operations = compressed_data_group
.into_par_iter()
.map(|(data, i, operation)| {
let mut writer = output.reopen_boxed()?;
writer.seek(SeekFrom::Start(operation.data_offset.unwrap()))?;
writer.write_all(&data)?;
Ok(i..i + 1)
})
.collect::<Result<Vec<_>>>()?;
modified_operations.extend(modified_group_operations);
}
let digest_uncompressed = context_uncompressed.finish();
partition_info.hash = Some(digest_uncompressed.as_ref().to_vec());
Ok(util::merge_overlapping(&modified_operations))
}
+1 -1
View File
@@ -13,12 +13,12 @@
// We use pb-rs' nostd mode. See build.rs.
extern crate alloc;
pub mod boot;
pub mod cli;
pub mod crypto;
pub mod escape;
pub mod format;
pub mod octal;
pub mod patch;
pub mod protobuf;
pub mod stream;
pub mod util;
+15 -3
View File
@@ -8,9 +8,11 @@ use std::sync::{
Arc,
};
use anyhow::Result;
use tracing::error;
fn main() -> Result<()> {
static LOGGING_INITIALIZED: AtomicBool = AtomicBool::new(false);
fn main() {
// Set up a cancel signal so we can properly clean up any temporary files.
let cancel_signal = Arc::new(AtomicBool::new(false));
{
@@ -22,5 +24,15 @@ fn main() -> Result<()> {
.expect("Failed to set signal handler");
}
avbroot::cli::args::main(&cancel_signal)
match avbroot::cli::args::main(&LOGGING_INITIALIZED, &cancel_signal) {
Ok(_) => {}
Err(e) => {
if LOGGING_INITIALIZED.load(Ordering::SeqCst) {
error!("{e:?}");
} else {
eprintln!("{e:?}");
}
std::process::exit(1);
}
}
}
File diff suppressed because it is too large Load Diff
+8
View File
@@ -0,0 +1,8 @@
/*
* SPDX-FileCopyrightText: 2023 Andrew Gunnerson
* SPDX-License-Identifier: GPL-3.0-only
*/
pub mod boot;
pub mod otacert;
pub mod system;
+150
View File
@@ -0,0 +1,150 @@
/*
* SPDX-FileCopyrightText: 2023 Andrew Gunnerson
* SPDX-License-Identifier: GPL-3.0-only
*/
use std::{borrow::Cow, cmp::Ordering, io::Cursor};
use bitflags::bitflags;
use thiserror::Error;
use tracing::trace;
use x509_cert::{der::asn1::BitString, Certificate};
use zip::{result::ZipError, write::FileOptions, CompressionMethod, ZipWriter};
use crate::{crypto, format::ota};
#[derive(Debug, Error)]
pub enum Error {
#[error("New otacerts.zip is too small to pad to {0} bytes")]
ZipTooSmall(usize),
#[error("New otacerts.zip is too large to fit in {0} bytes")]
ZipTooLarge(usize),
#[error("Crypto error")]
Crypto(#[from] crypto::Error),
#[error("x509 DER error")]
Der(#[from] x509_cert::der::Error),
#[error("Zip error")]
Zip(#[from] ZipError),
}
type Result<T> = std::result::Result<T, Error>;
/// Pad a non-zip64 zip file to the specified size by adding null bytes to the
/// archive comment field.
pub fn pad_zip(data: &mut Vec<u8>, size: usize) -> Result<()> {
match size.cmp(&data.len()) {
Ordering::Equal => Ok(()),
Ordering::Less => Err(Error::ZipTooLarge(size)),
Ordering::Greater => {
let padding = size - data.len();
if data.len() < 22
|| &data[data.len() - 22..][..4] != ota::ZIP_EOCD_MAGIC
|| padding > usize::from(u16::MAX)
{
return Err(Error::ZipTooSmall(size));
}
// Rewrite the comment size and pad with null bytes.
data.pop();
data.pop();
data.extend((padding as u16).to_le_bytes());
data.resize(size, 0);
Ok(())
}
}
}
bitflags! {
/// Android uses X.509 as nothing more than a file format to transport RSA
/// public keys. This is true for both the framework's RecoverySystem and
/// recovery's otautil/verifier.cpp. The only fields that must exist are
/// the public key and the signature algorithm. The rest can be removed with
/// no side effects whatsoever.
#[derive(Debug, Clone, Copy)]
pub struct OtaCertBuildFlags: u8 {
const COMPRESS_DEFLATE = 1 << 0;
const REMOVE_SIGNATURE = 1 << 1;
const REMOVE_EXTENSIONS = 1 << 2;
const REMOVE_ISSUER = 1 << 3;
const REMOVE_SUBJECT = 1 << 4;
}
}
/// Create an `otacerts.zip` file containing the specified certificate.
pub fn create_zip(cert: &Certificate, flags: OtaCertBuildFlags) -> Result<Vec<u8>> {
let raw_writer = Cursor::new(Vec::new());
let mut writer = ZipWriter::new(raw_writer);
let compression_method = if flags.contains(OtaCertBuildFlags::COMPRESS_DEFLATE) {
CompressionMethod::Deflated
} else {
CompressionMethod::Stored
};
let options = FileOptions::default().compression_method(compression_method);
writer.start_file("ota.x509.pem", options)?;
let cert = if flags.is_empty() {
Cow::Borrowed(cert)
} else {
let mut modified = cert.clone();
if flags.contains(OtaCertBuildFlags::REMOVE_SIGNATURE) {
modified.signature = BitString::from_bytes(&[])?;
}
if flags.contains(OtaCertBuildFlags::REMOVE_EXTENSIONS) {
if let Some(extensions) = &mut modified.tbs_certificate.extensions {
extensions.clear();
}
}
if flags.contains(OtaCertBuildFlags::REMOVE_ISSUER) {
modified.tbs_certificate.issuer.0.clear();
modified.tbs_certificate.issuer_unique_id = None;
}
if flags.contains(OtaCertBuildFlags::REMOVE_SUBJECT) {
modified.tbs_certificate.subject.0.clear();
modified.tbs_certificate.subject_unique_id = None;
}
Cow::Owned(modified)
};
crypto::write_pem_cert(&mut writer, &cert)?;
let raw_writer = writer.finish()?;
Ok(raw_writer.into_inner())
}
/// Create an `otacerts.zip` file padded to the specified size.
///
/// This will incrementally remove unneeded components from the certificate to
/// meet the size limit if needed.
pub fn create_zip_with_size(cert: &Certificate, size: usize) -> Result<Vec<u8>> {
let mut flags = OtaCertBuildFlags::empty();
for additional_flag in [
OtaCertBuildFlags::empty(),
OtaCertBuildFlags::COMPRESS_DEFLATE,
OtaCertBuildFlags::REMOVE_SIGNATURE,
OtaCertBuildFlags::REMOVE_EXTENSIONS,
OtaCertBuildFlags::REMOVE_ISSUER,
OtaCertBuildFlags::REMOVE_SUBJECT,
] {
flags |= additional_flag;
trace!("Attempting to create {size} byte otacerts.zip: {flags:?}");
let mut data = create_zip(cert, flags)?;
if data.len() <= size {
trace!("Padding {} byte otacerts.zip to {size}", data.len());
pad_zip(&mut data, size)?;
return Ok(data);
}
}
Err(Error::ZipTooLarge(size))
}
+239
View File
@@ -0,0 +1,239 @@
/*
* SPDX-FileCopyrightText: 2023-2024 Andrew Gunnerson
* SPDX-License-Identifier: GPL-3.0-only
*/
use std::{
io::{self, Cursor, SeekFrom},
ops::Range,
sync::atomic::AtomicBool,
};
use memchr::memmem;
use rayon::iter::{IntoParallelIterator, ParallelIterator};
use thiserror::Error;
use tracing::{debug, debug_span, trace, Span};
use x509_cert::Certificate;
use zip::ZipArchive;
use crate::{
crypto::RsaSigningKey,
format::{
avb::{self, AppendedDescriptorMut, Footer},
ota,
},
patch::otacert,
stream::{self, ReadSeekReopen, SectionReader, WriteSeekReopen},
util,
};
#[derive(Debug, Error)]
pub enum Error {
#[error("Old otacerts.zip not found in image")]
OldZipNotFound,
#[error("Image has no vbmeta footer")]
NoFooter,
#[error("No hash tree descriptor found in vbmeta header")]
NoHashTreeDescriptor,
#[error("{0:?} field is out of bounds")]
FieldOutOfBounds(&'static str),
#[error("AVB error")]
Avb(#[from] avb::Error),
#[error("OTA certificate error")]
OtaCert(#[from] otacert::Error),
#[error("I/O error")]
Io(#[from] io::Error),
}
type Result<T> = std::result::Result<T, Error>;
/// Find the bounds of a non-zip64 zip starting from the EOCD magic offset.
fn find_zip_bounds(data: &[u8], eocd_offset: usize) -> Option<Range<usize>> {
let eocd = &data[eocd_offset..];
if eocd.len() < 22 {
trace!("Buffer is too small to contain EOCD");
return None;
}
let cd_size = u32::from_le_bytes(eocd[12..16].try_into().unwrap()) as usize;
let cd_offset = u32::from_le_bytes(eocd[16..20].try_into().unwrap()) as usize;
let comment_size = usize::from(u16::from_le_bytes(eocd[20..22].try_into().unwrap()));
let start = eocd_offset.checked_sub(cd_size)?.checked_sub(cd_offset)?;
let end = eocd_offset.checked_add(22)?.checked_add(comment_size)?;
if end > data.len() {
trace!("End of zip is out of bounds");
return None;
}
trace!("Found zip bounds: {:?}", start..end);
let reader = SectionReader::new(Cursor::new(data), start as u64, (end - start) as u64).ok()?;
let mut zip_reader = ZipArchive::new(reader).ok()?;
if zip_reader.is_empty() {
// otacerts.zip files contain at least one cert.
trace!("Zip is empty");
return None;
}
for index in 0..zip_reader.len() {
let entry = zip_reader.by_index_raw(index).ok()?;
if !entry.name().ends_with(".x509.pem") {
// otacerts.zip files only contain files named this way.
trace!("Excluded due to invalid name: {:?}", entry.name());
return None;
}
}
debug!("Found otacerts.zip candidate");
// There's one or more entries and every one is named *.x509.pem.
Some(start..end)
}
/// Replace `otacerts.zip` with a new one containing the new certificate, but
/// padded to the same size. If the new zip is too large, the certificate will
/// be modified to remove unnecessary components until it fits. All operations
/// run in parallel where possible. The input and output must refer to the same
/// file and will be reopened from multiple threads.
///
/// Returns two sorted and non-overlapping lists of byte ranges that were
/// modified. The first list are the byte regions within the filesystem data
/// that contained otacerts.zip. The second list is the list of byte regions
/// outside of the filesyste, like the hash tree, FEC data, and AVB metadata.
///
/// If [`Error::OldZipNotFound`] is returned, the output will not have been
/// modified.
#[allow(clippy::type_complexity)]
pub fn patch_system_image(
input: &(dyn ReadSeekReopen + Sync),
output: &(dyn WriteSeekReopen + Sync),
certificate: &Certificate,
key: &RsaSigningKey,
cancel_signal: &AtomicBool,
) -> Result<(Vec<Range<u64>>, Vec<Range<u64>>)> {
// This must be a multiple of normal filesystem block sizes (eg. 4 KiB).
// This ensures that the block containing otacerts.zip's data won't cross
// chunk boundaries.
const CHUNK_SIZE: u64 = 2 * 1024 * 1024;
let parent_span = Span::current();
let (mut header, footer, image_size) = avb::load_image(input.reopen_boxed()?)?;
let Some(mut footer) = footer else {
return Err(Error::NoFooter);
};
let AppendedDescriptorMut::HashTree(descriptor) = header.appended_descriptor_mut()? else {
return Err(Error::NoHashTreeDescriptor);
};
let num_chunks = footer.original_image_size.div_ceil(CHUNK_SIZE);
trace!("Parallel heuristics search for otacerts.zip with {num_chunks} chunks");
let modified_ranges = (0..num_chunks)
.into_par_iter()
.map(|chunk| -> Result<Vec<Range<u64>>> {
stream::check_cancel(cancel_signal)?;
let offset = chunk * CHUNK_SIZE;
let size = CHUNK_SIZE.min(footer.original_image_size - offset);
let mut buf = vec![0u8; size as usize];
let mut reader = input.reopen_boxed()?;
reader.seek(SeekFrom::Start(offset))?;
reader.read_exact(&mut buf)?;
let mut writer = output.reopen_boxed()?;
let mut ranges = Vec::<Range<u64>>::new();
for eocd_offset_rel in memmem::find_iter(&buf, ota::ZIP_EOCD_MAGIC) {
let _span = debug_span!(parent: &parent_span, "otacerts", offset, eocd_offset_rel)
.entered();
let Some(bounds_rel) = find_zip_bounds(&buf, eocd_offset_rel) else {
continue;
};
let zip_size = bounds_rel.end - bounds_rel.start;
let new_zip = otacert::create_zip_with_size(certificate, zip_size)?;
let bounds = offset + bounds_rel.start as u64..offset + bounds_rel.end as u64;
stream::check_cancel(cancel_signal)?;
writer.seek(SeekFrom::Start(bounds.start))?;
writer.write_all(&new_zip)?;
ranges.push(bounds);
}
Ok(ranges)
})
.try_reduce(Vec::new, |mut result, item| {
result.extend(item);
Ok(result)
})?;
if modified_ranges.is_empty() {
return Err(Error::OldZipNotFound);
}
let update_ranges = if descriptor.hash_algorithm == "sha1" {
// Promote to a secure algorithm. SHA1 is allowed for verification only.
// The entire hash tree and FEC data will need to be recomputed.
let new_algorithm = "sha256".to_owned();
debug!(
"Changing insecure hash algorithm {} to {new_algorithm}",
descriptor.hash_algorithm,
);
descriptor.hash_algorithm = new_algorithm;
None
} else {
// Only need to update the hash tree and FEC data corresponding to the
// modified regions.
Some(modified_ranges.as_slice())
};
descriptor.update(input, output, update_ranges, cancel_signal)?;
if !header.public_key.is_empty() {
debug!("Signing system image");
header.set_algo_for_key(key)?;
header.sign(key)?;
}
let writer = output.reopen_boxed()?;
avb::write_appended_image(writer, &header, &mut footer, Some(image_size))?;
let AppendedDescriptorMut::HashTree(descriptor) = header.appended_descriptor_mut()? else {
return Err(Error::NoHashTreeDescriptor);
};
// The hash tree, FEC data, and AVB regions will have been modified.
let hash_tree_end = descriptor
.tree_offset
.checked_add(descriptor.tree_size)
.ok_or_else(|| Error::FieldOutOfBounds("hash_tree_end"))?;
let fec_data_end = descriptor
.fec_offset
.checked_add(descriptor.fec_size)
.ok_or_else(|| Error::FieldOutOfBounds("fec_data_end"))?;
let header_end = footer
.vbmeta_offset
.checked_add(footer.vbmeta_size)
.ok_or_else(|| Error::FieldOutOfBounds("avb_end"))?;
let footer_start = image_size - Footer::SIZE as u64;
let other_ranges = util::merge_overlapping(&[
descriptor.tree_offset..hash_tree_end,
descriptor.fec_offset..fec_data_end,
footer.vbmeta_offset..header_end,
footer_start..image_size,
]);
Ok((modified_ranges, other_ranges))
}
+94 -1
View File
@@ -3,7 +3,7 @@
* SPDX-License-Identifier: GPL-3.0-only
*/
use std::{fmt, path::Path};
use std::{cmp::Ordering, fmt, ops::Range, path::Path};
use num_traits::PrimInt;
@@ -48,3 +48,96 @@ pub fn parent_path(path: &Path) -> &Path {
Path::new(".")
}
/// Sort and merge overlapping intervals.
pub fn merge_overlapping<T>(sections: &[Range<T>]) -> Vec<Range<T>>
where
T: Ord + Clone + Copy,
{
let mut sections = sections.to_vec();
sections.sort_by_key(|r| (r.start, r.end));
let mut result = Vec::<Range<T>>::new();
for section in sections {
if section.start >= section.end {
continue;
} else if let Some(last) = result.last_mut() {
if section.start <= last.end {
last.end = last.end.max(section.end);
continue;
}
}
result.push(section);
}
result
}
/// Binary search to determine if the needle overlaps any of the ranges.
pub fn ranges_overlaps<T>(ranges: &[Range<T>], needle: &Range<T>) -> bool
where
T: Ord,
{
if needle.start < needle.end {
ranges
.binary_search_by(|range| {
if range.start > needle.end {
Ordering::Greater
} else if range.end <= needle.start {
Ordering::Less
} else {
Ordering::Equal
}
})
.is_ok()
} else {
false
}
}
/// Binary search to determine if any of the ranges contain the needle.
pub fn ranges_contains<T>(ranges: &[Range<T>], needle: &T) -> bool
where
T: Ord,
{
ranges
.binary_search_by(|range| {
if range.start > *needle {
Ordering::Greater
} else if range.end <= *needle {
Ordering::Less
} else {
Ordering::Equal
}
})
.is_ok()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_ranges_overlaps() {
assert_eq!(ranges_overlaps(&[0..4], &(0..0)), false);
assert_eq!(ranges_overlaps(&[0..4], &(0..4)), true);
assert_eq!(ranges_overlaps(&[0..4], &(1..4)), true);
assert_eq!(ranges_overlaps(&[0..4], &(0..3)), true);
assert_eq!(ranges_overlaps(&[0..4], &(4..5)), false);
assert_eq!(ranges_overlaps(&[5..8], &(5..9)), true);
assert_eq!(ranges_overlaps(&[5..8], &(4..8)), true);
assert_eq!(ranges_overlaps(&[5..8], &(4..9)), true);
assert_eq!(ranges_overlaps(&[0..4, 5..8], &(4..5)), true);
assert_eq!(ranges_overlaps(&[0..4, 5..8], &(0..9)), true);
}
#[test]
fn test_ranges_contains() {
assert_eq!(ranges_contains(&[0..4], &0), true);
assert_eq!(ranges_contains(&[0..4], &4), false);
assert_eq!(ranges_contains(&[0..4, 5..8], &4), false);
assert_eq!(ranges_contains(&[0..4, 5..8], &6), true);
}
}
+398 -111
View File
@@ -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,11 +14,16 @@ use rsa::RsaPrivateKey;
use avbroot::{
self,
format::avb::{self, AppendedDescriptorMut, AppendedDescriptorRef},
stream::{self, SharedCursor},
crypto::RsaSigningKey,
format::avb::{
self, AlgorithmType, AppendedDescriptorMut, AppendedDescriptorRef,
ChainPartitionDescriptor, Descriptor, Footer, HashDescriptor, HashTreeDescriptor, Header,
KernelCmdlineDescriptor, PropertyDescriptor,
},
stream::SharedCursor,
};
fn get_test_key() -> RsaPrivateKey {
fn get_test_key() -> RsaSigningKey {
let data = include_str!(concat!(
env!("CARGO_WORKSPACE_DIR"),
"/e2e/keys/TEST_KEY_DO_NOT_USE_avb.key",
@@ -28,157 +33,439 @@ fn get_test_key() -> RsaPrivateKey {
"/e2e/keys/TEST_KEY_DO_NOT_USE_avb.passphrase",
));
RsaPrivateKey::from_pkcs8_encrypted_pem(data, passphrase.trim_end()).unwrap()
let key = RsaPrivateKey::from_pkcs8_encrypted_pem(data, passphrase.trim_end()).unwrap();
RsaSigningKey::Internal(key)
}
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.
avb::write_appended_image(&mut writer, &header, &mut footer, image_size).unwrap();
let new_data = writer.into_inner();
// Write vbmeta structures.
avb::write_appended_image(&mut writer, &header, &mut footer, Some(image_size)).unwrap();
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"),
}
fn round_trip_appended_hash_tree_image_fixed_size() {
let image_size = 32768;
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, &cancel_signal).unwrap();
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.
avb::write_appended_image(&mut writer, &header, &mut footer, image_size).unwrap();
let mut new_data = Vec::new();
// Write vbmeta structures.
avb::write_appended_image(&mut writer, &header, &mut footer, Some(image_size)).unwrap();
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(),
[
0xb5, 0x56, 0x65, 0x81, 0x5a, 0x16, 0x65, 0xa9, 0xa6, 0xc6, 0x9e, 0x41, 0x89, 0x9f,
0xe9, 0xbc, 0xea, 0x59, 0x4d, 0x14, 0x8a, 0x9e, 0x2b, 0x13, 0xa0, 0x3a, 0x8e, 0xd4,
0x59, 0xcd, 0x74, 0xe7, 0x99, 0xbd, 0xa3, 0x58, 0x4b, 0x84, 0xf2, 0x04, 0xe2, 0x12,
0x48, 0xfe, 0x4f, 0x67, 0x1f, 0x2a, 0xaa, 0x22, 0x51, 0x19, 0x83, 0x95, 0xa8, 0x03,
0xf5, 0x87, 0x12, 0x05, 0x8e, 0x14, 0xd9, 0xbd
],
);
// 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_minimum_size() {
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);
// Write the raw partition data.
writer.write_all(&raw_data).unwrap();
// Generate and write the hash tree and FEC data.
match header.appended_descriptor_mut().unwrap() {
AppendedDescriptorMut::HashTree(d) => {
d.update(&writer, &writer, None, &cancel_signal).unwrap();
}
AppendedDescriptorMut::Hash(_) => panic!("Expected hash tree descriptor"),
}
// 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 vbmeta structures.
avb::write_appended_image(&mut writer, &header, &mut footer, None).unwrap();
let mut data = Vec::new();
writer.rewind().unwrap();
writer.read_to_end(&mut data).unwrap();
// 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, 28672);
}
+288 -62
View File
@@ -1,5 +1,5 @@
/*
* SPDX-FileCopyrightText: 2023 Andrew Gunnerson
* SPDX-FileCopyrightText: 2023-2024 Andrew Gunnerson
* SPDX-License-Identifier: GPL-3.0-only
*/
@@ -7,13 +7,20 @@ use std::io::Cursor;
use avbroot::{
self,
format::bootimage::{BootImage, BootImageExt},
crypto::RsaSigningKey,
format::{
avb::{AlgorithmType, Descriptor, HashDescriptor, Header},
bootimage::{
self, BootImage, BootImageExt, BootImageV0Through2, BootImageV3Through4, RamdiskMeta,
V1Extra, V2Extra, V4Extra, VendorBootImageV3Through4, VendorV4Extra,
},
},
stream::{FromReader, ToWriter},
};
use pkcs8::DecodePrivateKey;
use rsa::RsaPrivateKey;
fn get_test_key() -> RsaPrivateKey {
fn get_test_key() -> RsaSigningKey {
let data = include_str!(concat!(
env!("CARGO_WORKSPACE_DIR"),
"/e2e/keys/TEST_KEY_DO_NOT_USE_avb.key",
@@ -23,103 +30,322 @@ fn get_test_key() -> RsaPrivateKey {
"/e2e/keys/TEST_KEY_DO_NOT_USE_avb.passphrase",
));
RsaPrivateKey::from_pkcs8_encrypted_pem(data, passphrase.trim_end()).unwrap()
let key = RsaPrivateKey::from_pkcs8_encrypted_pem(data, passphrase.trim_end()).unwrap();
RsaSigningKey::Internal(key)
}
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
View File
@@ -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.
+28 -8
View File
@@ -1,23 +1,44 @@
[advisories]
vulnerability = "deny"
unmaintained = "deny"
version = 2
yanked = "deny"
notice = "deny"
ignore = [
# https://rustsec.org/advisories/RUSTSEC-2023-0071
#
# This is a side-channel vulnerability where secrets can be leaked to an
# attacker that is able to measure the timing of a large number of RSA
# operations. As of 2023-12-03, there is no released version of the rsa
# crate that contains a fix.
#
# For avbroot specifically, this vulnerability is not too critical for a
# couple reasons:
#
# 1. avbroot performs RSA signing only at the end of lengthy processes
# that involve a lot of disk I/O. It's very expensive to run avbroot
# the millions of times needed to capture a sufficient amount of timing
# data.
# 2. During a single run of avbroot, it will only perform RSA signing a
# handful of times. To get sufficient measurements, the attacker would
# need to rerun avbroot. If they are able to rerun avbroot, then they
# are also able to just read and steal the private key directly.
#
# avbroot has no network capabilities, so this is not inherently remotely
# exploitable.
"RUSTSEC-2023-0071",
]
[licenses]
version = 2
include-dev = true
unlicensed = "deny"
allow = [
"Apache-2.0",
"Apache-2.0 WITH LLVM-exception",
"BSD-3-Clause",
"GPL-3.0",
"ISC",
"MIT",
"OpenSSL",
"Unicode-DFS-2016",
]
copyleft = "allow"
default = "deny"
[[licenses.clarify]]
name = "ring"
@@ -47,7 +68,6 @@ bypass = [
unknown-registry = "deny"
unknown-git = "deny"
allow-git = [
"https://github.com/chenxiaolong/xz2-rs",
"https://github.com/chenxiaolong/bzip2-rs",
"https://github.com/chenxiaolong/zip",
"https://github.com/jongiddy/bzip2-rs",
]
-1
View File
@@ -1 +0,0 @@
/files/
+9 -2
View File
@@ -10,18 +10,25 @@ publish = false
[dependencies]
anyhow = "1.0.75"
attohttpc = "0.26.1"
avbroot = { path = "../avbroot" }
clap = { version = "4.4.1", features = ["derive"] }
ctrlc = "3.4.0"
hex = { version = "0.4.3", features = ["serde"] }
ring = "0.17.0"
rsa = { version = "0.9.6", features = ["hazmat"] }
serde = { version = "1.0.188", features = ["derive"] }
tempfile = "3.8.0"
toml_edit = { version = "0.20.1", features = ["serde"] }
toml_edit = { version = "0.22.9", features = ["serde"] }
topological-sort = "0.2.2"
tracing = "0.1.40"
tracing-subscriber = "0.3.18"
x509-cert = "0.2.5"
# https://github.com/zip-rs/zip/pull/383
[dependencies.zip]
git = "https://github.com/chenxiaolong/zip"
rev = "989101f9384b9e94e36e6e9e0f51908fdf98bde6"
default-features = false
[features]
static = ["avbroot/static"]
+12 -50
View File
@@ -1,64 +1,26 @@
# End-to-end tests
avbroot's output file is reproducible for a given input file. [`e2e.toml`](./e2e.toml) lists some OTA images with unique properties and the expected checksums before and after patching. These tests use pregenerated, hardcoded test keys for signing. **These keys should NEVER be used for any other purpose.**
avbroot's output file is reproducible for a given input file. [`e2e.toml`](./e2e.toml) lists some profiles for generating mock OTA images with unique properties and the expected checksums before and after patching. These tests use pregenerated, hardcoded test keys for signing. **These keys should NEVER be used for any other purpose.**
For each image listed in the config, the test process will:
For each profile listed in the config, the test process will:
1. Download the OTA if it doesn't already exist in `./files/<device>/` (or the workdir specified by `-w`)
2. Verify the OTA checksum
3. Run avbroot against the OTA using `--magisk`
4. Extract the AVB-related partitions from the patched OTA and verify their checksums
5. Verify the patched OTA checksum
1. Generate a mock OTA based on the specification
2. Verify tha original OTA checksum
3. Run avbroot against the OTA using `--magisk` (with a mock Magisk APK)
4. Verify the patched OTA checksum
5. Extract the AVB-related partitions from the patched OTA
6. Run avbroot against the OTA again using `--prepatched`
7. Verify the patched OTA checksum again
For more efficient CI testing, the tests can operate on "stripped" OTAs. A stripped OTA is identical to the full OTA, except that partitions in `payload.bin` unrelated to AVB are zeroed out. This reduces the download size and disk space requirements by a couple orders of magnitude. **A stripped OTA is NOT bootable and should never be flashed on a real device.**
The default profiles shipped with the project mimic how various stock OTAs for Pixel devices are built. The generated mock OTAs have valid signatures and data structures for all components, but without any actual data where possible. For example, most files in the ramdisks are empty files. To ensure the mock OTAs cannot be mistakenly installed on a real device, the OTA metadata lists a fake device name in the preconditions section.
## Running the tests
To test against the device OTA images listed in [`e2e.toml`](./e2e.toml), run:
To test against the profiles listed in [`e2e.toml`](./e2e.toml), run:
```bash
# To test all device OTAs
# To test all profiles
cargo run --release -- test -a
# Or to test against specific device OTAs
cargo run --release -- test -d cheetah -d bluejay
# Or to test against specific profiles
cargo run --release -- test -p pixel_v4_gki -p pixel_v4_non_gki
```
To test against stripped OTAs (smaller download, but not bootable), pass in `--stripped`.
## Downloading a device image
To download a full OTA image, run:
```bash
cargo run --release -- download -d <device>
```
This normally happens automatically when running the `test` subcommand. To download the stripped OTA image instead, pass in `--stripped`.
If the image file does not already exist, then it will be downloaded and the checksums will be validated. If the download is interrupted, it will automatically resume when the command is rerun. If the file is already downloaded, the command is effectively a no-op unless `--revalidate` is passed in to revalidate the image checksums.
## Adding a new device image
To add a new device image to the testing configuration, run:
```bash
cargo run --release -- add -d <device> -u <full OTA URL> -H <expected checksum>
```
If the OS vendor does not provide a SHA-256 checksum, omit `-H` and the program will compute the checksum from the downloaded data.
This process will download the full OTA, strip it, patch the full OTA, patch the stripped OTA, extract the AVB partitions, and write all of the checksums to [`e2e.toml`](./e2e.toml).
The process for updating an existing device config is exactly the same as adding a new one.
## Stripping a full OTA
To convert a full OTA to the stripped form, run:
```bash
cargo run --release -- strip -i <input zip> -o <output zip>
```
This normally happens automatically as a part of adding a new device image.
+135 -103
View File
@@ -1,116 +1,148 @@
[magisk]
"url" = "https://github.com/topjohnwu/Magisk/releases/download/v26.3/Magisk.v26.3.apk"
"hash" = "30ff6ec0709412adfcd0b735c0eb1f61cd9d589af4bdef4cf03c09b986b5acce"
# Metadata used when generating OTAs. These values don't affect behavior at all.
[ota_info]
# Make sure generated OTAs aren't flashable on real devices.
device = "avbroot_fake_device"
fingerprint = "avbroot/avbroot_fake_device:14/UQ1A.240101.000/12345678:user/release-keys"
build_number = "UQ1A.240101.000"
incremental_version = "12345678"
android_version = "14"
sdk_version = "34"
security_patch_level = "2024-01-01"
# Google Pixel 7 Pro
# What's unique: init_boot (boot v4) + vendor_boot (vendor v4)
[device.cheetah]
url = "https://dl.google.com/dl/android/aosp/cheetah-ota-tq3a.230901.001-6b881553.zip"
sections = [
{ start = 0, end = 152523 },
{ start = 21750700, end = 23251455 },
{ start = 2044485458, end = 2044493782 },
{ start = 2316042420, end = 2333812318 },
{ start = 2344911202, end = 2344916318 },
]
hash.original.full = "6b881553f012d582080642d660e1cf5c9e6fe41e9f1c6ab12ae87fab7894e307"
hash.original.stripped = "9befd7887a125ebd8e9ae0555469dababe6bc04b0aa41aa2562036782a6d87e0"
hash.patched.full = "91e15447ade648c10bce599e75569ce55edc18798b11a704586acf3b51ae7971"
hash.patched.stripped = "84b069ac7f20115ac0b19d7e319bb86d00b9f3f64e0c7a51612f99d6cae297bb"
hash.avb_images."init_boot.img" = "3bedb41be98c46241f11219021dfbb799a6d5c89e6e00d45a66f9a5b42e7dfbc"
hash.avb_images."vbmeta.img" = "e8e6e898ca73807edb43af0a0e86d4a94b14256def89581970287a9b1bf7a3ee"
hash.avb_images."vbmeta_system.img" = "dbb63e08f26f46ccda501d99058d513ff71e3d6302c14d587442b666ff08862a"
hash.avb_images."vbmeta_vendor.img" = "6ffa0a10e72c3371653be80de1380832b4d7f8bbf38a2bd861d44a4097a57117"
hash.avb_images."vendor_boot.img" = "dd58e8d46dd26198edf14f72d17a3315ff4c2aeb65b98dbd06369ca2a6e365a3"
[profile.pixel_v4_gki.partitions.boot]
avb.signed = true
data.type = "boot"
data.version = "v4"
data.kernel = true
[profile.pixel_v4_gki.partitions.init_boot]
avb.signed = true
data.type = "boot"
data.version = "v4"
data.ramdisks = [["init", "first_stage"]]
[profile.pixel_v4_gki.partitions.system]
avb.signed = false
data.type = "dm_verity"
data.content = "system_otacerts"
[profile.pixel_v4_gki.partitions.vbmeta]
avb.signed = true
data.type = "vbmeta"
data.deps = ["boot", "init_boot", "vendor_boot", "vbmeta_system"]
[profile.pixel_v4_gki.partitions.vbmeta_system]
avb.signed = true
data.type = "vbmeta"
data.deps = ["system"]
[profile.pixel_v4_gki.partitions.vendor_boot]
avb.signed = false
data.type = "boot"
data.version = "vendor_v4"
data.ramdisks = [["otacerts", "first_stage", "dsu_key_dir"]]
[profile.pixel_v4_gki.hashes]
original = "6b140c378d21eae2fa4fc581bce13a689b21bd32f5fba865698d1fd322f2f8c6"
patched = "a68b3a44c0cf015a225f92837c05fd0dec6e159584c5880eff30d12daa7123ff"
# Google Pixel 6a
# What's unique: boot (boot v4, no ramdisk) + vendor_boot (vendor v4, 2 ramdisks)
[device.bluejay]
url = "https://dl.google.com/dl/android/aosp/bluejay-ota-tq3a.230901.001-1f1f0abe.zip"
sections = [
{ start = 0, end = 142587 },
{ start = 1062619, end = 21678441 },
{ start = 1920893927, end = 1920902079 },
{ start = 2103596124, end = 2126776858 },
{ start = 2133324030, end = 2133329146 },
]
hash.original.full = "1f1f0abe67a6f6f47287be6dafec2c12628de6a715b82ca7beddaf67ad22aca5"
hash.original.stripped = "38b15f5efdc7e056bc799859ba72ef9a73e93c61292c59f85fb4b9c31acc5f82"
hash.patched.full = "65f7e29591fb48ad9c7c3233df4d76bb6986aef96b94c06b73213477bc7486d8"
hash.patched.stripped = "cf77b5e307ce4d2a62e742cd3a300964de6c693880cbfa90dcd0bfec66bd1976"
hash.avb_images."boot.img" = "a19cb4d4fcc7f3e7d3046c3d19e2f243fb02513ca848ff92ad70d1ada55c4e65"
hash.avb_images."vbmeta.img" = "2d817e35f7b6cdc2edce58ef249a966fc677085b70a4aced4c829320aeef0be2"
hash.avb_images."vbmeta_system.img" = "98a050f0d53a016fbb78147b1b4a9bca3fde615aa4da34bf62c2e07a395104b5"
hash.avb_images."vbmeta_vendor.img" = "fac530f47f237e76f3c7c3cdfe96308170dd8e8f0b227d81114a489c69ba763c"
hash.avb_images."vendor_boot.img" = "b3c596360f38cd0d6212341571acf3c2d977921f8bf163d7da27978a353da9f7"
[profile.pixel_v4_non_gki.partitions.boot]
avb.signed = true
data.type = "boot"
data.version = "v4"
data.kernel = true
[profile.pixel_v4_non_gki.partitions.system]
avb.signed = false
data.type = "dm_verity"
data.content = "system_otacerts"
[profile.pixel_v4_non_gki.partitions.vbmeta]
avb.signed = true
data.type = "vbmeta"
data.deps = ["boot", "vendor_boot", "vbmeta_system"]
[profile.pixel_v4_non_gki.partitions.vbmeta_system]
avb.signed = true
data.type = "vbmeta"
data.deps = ["system"]
[profile.pixel_v4_non_gki.partitions.vendor_boot]
avb.signed = false
data.type = "boot"
data.version = "vendor_v4"
data.ramdisks = [["init", "otacerts", "first_stage", "dsu_key_dir"], ["dlkm"]]
[profile.pixel_v4_non_gki.hashes]
original = "31963e6f81986c6686111f50e36b89e4d85ee5c02bc8e5ecd560528bc98d6fe7"
patched = "a13173a006ad94b25db682bb14fde2e36891417727d6541bdf5f9bca57d26751"
# Google Pixel 4a 5G
# What's unique: boot (boot v3) + vendor_boot (vendor v3)
[device.bramble]
url = "https://dl.google.com/dl/android/aosp/bramble-ota-tq3a.230901.001-6d107ffa.zip"
sections = [
{ start = 0, end = 142225 },
{ start = 497945, end = 11647087 },
{ start = 1683478307, end = 1683482723 },
{ start = 1917281683, end = 1940628772 },
{ start = 1943442548, end = 1943447665 },
]
hash.original.full = "6d107ffac1cd3da2c972112acc75957ed725e5c13d57ca724d9bcca5404fcebd"
hash.original.stripped = "5b889bdab3bb12ddcd3c243a56e1c58bedada8831069f49d56fe5098fb141e35"
hash.patched.full = "8725e03798539070d7075a07c80fb1403652a2b446d5c460d636a32b7368c9a2"
hash.patched.stripped = "c4b5cfa84dc8c15f3ac9661d768062546fa36ca82cdeb6dd943347be0422903c"
hash.avb_images."boot.img" = "8e7278a2e8ae44ffc5475717eb0e1aa56bfb7650aef34375b0fe92f790835f95"
hash.avb_images."vbmeta.img" = "b036132b867f52a86eef79716261f35b1eb50e843dc4fe42f72cce67b24ae2db"
hash.avb_images."vbmeta_system.img" = "9a7c6fd654e7a92aeffbdbd55ea0d87eee36f4c235e1b505423ad8a13a751a00"
hash.avb_images."vendor_boot.img" = "6e83d22371af4a26aef2c64cc4235f83b03978b91aef69c34eec1985e9942139"
[profile.pixel_v3.partitions.boot]
avb.signed = true
data.type = "boot"
data.version = "v3"
data.kernel = true
data.ramdisks = [["init"]]
[profile.pixel_v3.partitions.system]
avb.signed = false
data.type = "dm_verity"
data.content = "system_otacerts"
[profile.pixel_v3.partitions.vbmeta]
avb.signed = true
data.type = "vbmeta"
data.deps = ["boot", "vendor_boot", "vbmeta_system"]
[profile.pixel_v3.partitions.vbmeta_system]
avb.signed = true
data.type = "vbmeta"
data.deps = ["system"]
[profile.pixel_v3.partitions.vendor_boot]
avb.signed = false
data.type = "boot"
data.version = "vendor_v3"
data.ramdisks = [["otacerts", "first_stage", "dsu_key_dir"]]
[profile.pixel_v3.hashes]
original = "e684aacb54464098c1b8e3f499efe35dff10ea792e89d71a83404620d0108b3e"
patched = "49e810ae76154bccf2dc7d810b9eec19d23a5b8fa32381469660488d0a25121b"
# Google Pixel 4a
# What's unique: boot (boot v2)
[device.sunfish]
url = "https://dl.google.com/dl/android/aosp/sunfish-ota-tq3a.230805.001-01fd34b2.zip"
sections = [
{ start = 0, end = 131325 },
{ start = 478553, end = 34014760 },
{ start = 1658403115, end = 1658407531 },
{ start = 1855778567, end = 1855783585 },
]
hash.original.full = "01fd34b206152a3559039161c9874ab03df37da4268b86a9e0be899de5fc0af7"
hash.original.stripped = "cc311b5bd46e06cfdefbade794d33aa9bc3ceda4ad4f38bfe9f0dfc17033d207"
hash.patched.full = "f2ac798b31a94dc251ca4ce370ebfb3073170d4a34431829df9ed0742149ffe7"
hash.patched.stripped = "4387a5ba30c925f56c67eeaf6512757d5c1e07dfd2a8a99be14e06db8ba2dde7"
hash.avb_images."boot.img" = "506a955080b6cfa2039ef85923e8a4e717ef6c1dc478538599c7ba26ee21e525"
hash.avb_images."vbmeta.img" = "3679c7224e3e3e0793b4d1a031e116098460a6b4c5f3f88d5a3a0a4c65b21582"
hash.avb_images."vbmeta_system.img" = "1d3efa00fd1d44a594c7317072468fa95c23d83d2759d6d6e757783ceeabc594"
# OnePlus 10 Pro
# Build NE2215_11_C.26
# What's unique:
# - boot (boot v4) + recovery (boot v4)
# - boot images have VTS signature block filled with all 0s
# - payload.bin uses ZERO blocks
# Build info:
# - Unofficial list of full OTAs: https://forum.xda-developers.com/t/oneplus-10-pro-rom-ota-oxygen-os-repo-of-oxygen-os-builds.4572593/
# - The North American builds are used because they're the only ones hosted on
# a well known domain
# - The build number can be found in <my_manifest>/build.prop since it's not
# obvious from the filename
[device.ossi]
url = "https://android.googleapis.com/packages/ota-api/package/4cacbe5e6a3ab6a6fade68cc40f44d0fa6a2928a.zip"
sections = [
{ start = 0, end = 204048 },
{ start = 19105432, end = 34966775 },
{ start = 2657405750, end = 2657407254 },
{ start = 4984045446, end = 5006377281 },
{ start = 5114504441, end = 5114507197 },
{ start = 5138158449, end = 5138159817 },
{ start = 5140101511, end = 5140105324 },
]
hash.original.full = "929f892fbd70699cf7f118a119aac1ae1b86351e1ada17715666fa4401e63472"
hash.original.stripped = "4eabaf79b6c2b5df305e3ecdc2b9570c0dd27350b4e8d6434584000c4989ff3d"
hash.patched.full = "ec576b7430e5a2788e89f8bbe37ee81b116386d9b101865d3b75eb9c51a0db4c"
hash.patched.stripped = "ddd0cf1b42564f8c2980320c14f8e0eb125412eb7a13ae8fb52d35424546c235"
hash.avb_images."boot.img" = "480d7cf519326fcaa5106ecfbbeb907309068635f7f6a25e5e4571d525c4926a"
hash.avb_images."recovery.img" = "eeb0f67e2084174fced510f4b59a82022559ffbe1c20d2c0ea4757fcd989a3af"
hash.avb_images."vbmeta.img" = "c022cf79da301a8430af5c49704944c490707fa0306031fe3ea22c39ce4734f6"
hash.avb_images."vbmeta_system.img" = "749616b7f04487c05e9e363ad2071a0ab3bae29d497daf1f1a7695f7c8cfa82a"
hash.avb_images."vbmeta_vendor.img" = "a6037fce745384425fb12745b8568386b84fb57ca6f94f6e47bcf754de341ae4"
[profile.pixel_v2.partitions.boot]
avb.signed = false
data.type = "boot"
data.version = "v2"
data.kernel = true
data.ramdisks = [["init", "otacerts", "first_stage", "dsu_key_dir"]]
[profile.pixel_v2.partitions.system]
avb.signed = false
data.type = "dm_verity"
data.content = "system_otacerts"
[profile.pixel_v2.partitions.vbmeta]
avb.signed = true
data.type = "vbmeta"
data.deps = ["boot", "vbmeta_system"]
[profile.pixel_v2.partitions.vbmeta_system]
avb.signed = true
data.type = "vbmeta"
data.deps = ["system"]
[profile.pixel_v2.hashes]
original = "ee9568797d9195985f14753b89949d8ebb08c8863a32eceeeec6e8d94661b1cf"
patched = "e413f1f87ee5ba53d402edad03b0df8af451b5b0323202a9d32b8327d433d340"
+54
View File
@@ -0,0 +1,54 @@
-----BEGIN ENCRYPTED PRIVATE KEY-----
MIIJrTBXBgkqhkiG9w0BBQ0wSjApBgkrBgEEAdpHBAswHAQQjwBqCb7mn4vtIbEE
v/daiQICQAACAQgCAQEwHQYJYIZIAWUDBAEqBBAMdJ698WAn+aEvKOQ70wGjBIIJ
UIJbh4gk0vu1YP5FAPR6S1jvfSaf+hCmDq1mrK2nUt1sv5vDUFimKAJUYs0lLwuH
1FCB3QkXrn+2Z2wcJoeA1nq0pp39nvGu4EBBTAHgTebwfaqvfNvRE9kAjGjMWpux
BKaXxpSOC1V2JNfcxWi+reKwOweDzde9s5vpvPx7/637GPrLiMOvKWoUSg15jDbF
u8T/hfY2TC0KiRLLT9b/8DZrscjXH1jAM7e3FK9F1PT9DSO9SfEkH3YRGPwkcnrl
JcI6aPD0J3/YtXYrgcTdNNUdJy3Tbq328peXyw1ayumQTHfHahCNaIaKtH2WmKtx
xUcaZnFD+AxPT52Dl41XD69tN6tWSENuf0ruaD6HsRRcALH4X5xqLKsObA/t2b6S
m3bWcbKhuD5MGLcuw0oKbDuEYVJeZvuppsKhr2JsqqPBbnJc8FR59t2IGMYOteFV
HD6oX0YxIZU+Cb3R3xF7gO7cn/NxBJetph9FBhn1yLEqZnU2uWpU0+WFY64J8S/1
yDJsu7Sa9HShZpUQfLC/eRsQQQKaZrUsCHHC9MFFqy9zmsn1lIFUbqAJ6MTe3cf4
QQJNhwNdBzSInBuI8sv5lwk6eidQyyRkaB+M8TrjhIwFRlCBd7XUbPACID+m/1jD
DvzB/iV8GHhDW0gf8jOObfGDA9zsDt9R1fVgb4ehimNPhDV8ILIELqREo9DWTr4Q
RGmjSwiyENi1Pp4PXS12NI0KbdJ5zuj5scwwLogs3Kdt3WMmqCZU6ektDeKuRI0W
ZnnCEiHo7jinMo869ZhyI3BQykuSfRH24JuPX+F3vxHmPoFYDjHG8ZCVWyZEMHPl
wRxok5k8jwe+sMOmRtQZPkcJFSiSYWyUciRED2oV1U5ojDo2LBvmQLjgPwCj/ECo
v9DwRzsrljwI7tpIqBmHbWxcSxdDhdQNiRk0PU0bunNh1ubJkoyj/FdEPHOvexpR
hCGzwDRqGRz5Mk73FI6ybCbBguo7m3ic6duMNhSYQdKTBPAilPjSpEB+a4awyjFv
sn5zVciwgti/CtZJiFDNeGAxr4V18OO8qdDVL8Za/SvqZrr0Y0MJgnWM4RuB6lBl
d+Rwub9ReJIhiAjTPIiJfHibxtDLEtXEfbSdE/a/Yn0xQYUYHKdbYJS0heumSF7D
ihvGedIpW16YjXCb052xCxeN05rDzbm1m7UJlFivSfo/GXgQSH4W9xzG8yLCid8L
PnptMXJFbI53mCJfjKy7ZV7rlx6dXf/uN620qwr/QCxBQfjM4ndA7X3UUAF/riHW
MgaY5+GcBhRhGug1IcIZG/p8QB0t8Lt6Xssu/87uVSsc9pKEymVZwdhy3gU/YZY0
dx6AsYk9E2n05g5AtafsDnik1gVgBb5KtMzW7XKYAA8805Ms4+gqeS9OUrevF6QS
mRFHJpvaNuEYR6Lp6jLfwPH6MK+tZbE42St8XGLbFFgp3ncSe6UBhoGCYN7/hJkw
QXPnK7vlgiO3NEV4d0IdZWRPUHXASYiEkpv2IYmUq8CYyqZuBERVs6eZmLIC7D9V
y6kORASxDl0wEl4Nc0B0iZ0jhMGiNmG9EO2ek+YwlpB+vBm1C/Z+30qpzAnbnh22
HWHn5g9koMa4oEkO7P5Bpp4UifxX/S5f0lAuLotaE6eYmwr9Tw7N9Za/mSLk2CuK
akc/T/ycgQdGb/wWHvCzvm3EsbuAAxFDqhwAoobhkdvSyQek8HYJ2FiiXHgFY9kI
RJ49AHCaiXwHInZwVzJd30NJFn1I/o9dLULwYPNsjfeULr5ZUSe95CYLL4LUxDFr
ecd0YiuoA1Y8gIwIRAaFAs1wdkNZoiEj3vH3LMFM6DMlvwDCB+vOpFwmLRQOT5Vq
DWPZTKNYkmU9ReWY6AM2VhmMZa+GHW7wbPKllPUCA/OmZQM98f1ivPXhADP+IAWt
1zNy+hqhrB/HV820t7m8SEK9GRAj0ARfpV6b9LsUH91TBY4MXKSZW9BDlu51Nwcl
zRZDIW1ZDA4cCI8JWnCALTNntuzcRx+y9pBRwxSOJXv6i08yOxDE7En1iiF79S6T
91jo20SLGeazQm1iun2eOh/49xplktxED5m/4/aAIjwDxXpbGT9VzOZ86P0/z32j
G0PngN173UGkmLDUZlaUavVrR3l85Uw6OxIgNMDaG8gQkbZT+iVLQs4MGiOJqFfc
UqusOXh2E3WUkX+5cWIdv3GGwBvKw6NE09u1+RJMvqHB3NlOruSazDyTVgPC/IkQ
iMSm1rGWdXQozaAur3jXQDYrmM4iV2ydDDcUc4FAgCwnT68073BemHvv9gPzTyex
Wr8N98nmTepypAcgARDI2wlwaGm3fO5kvaxOq4fiqYK7PoTUVsS6OCfafhR7MNVQ
lVpNHaDFS99ygjzjIvd2tQYY/Mwsvhp6YyMcXhtqTk0snlCGNFQh1rnDfEUFCGV7
EaraeuvTk5+ofTd9S8Qm+weYddRkHeZVpEFAOa/cL/cevek7yv60pXDUPrmlWD29
Ydu0VEO7DmVQnLm2gDprIQsZc01QxKRpCuRgwRgcpmAHX+xCFUWTCFL59mHW3F5x
7VCl79NXYaO6Rp7GV6xTgjD5FMe0pP51s8+dUwTCq3U5Y5KqScvvHiPaatb0DuSI
FcMzXRcapGrj5C42U2R7cpLq0xJi5QFapDD+wlEUs0lxZDiqf30Qr197a3KOOsWs
pyoR/Ytry0nyHxy4CFvA5hbWc7ng2ZegykCkBVS6XM2upUbWxbPihb3v5UgDb3zD
wXXwhjs+hwIh6xha7HlfH43HF8ucjLsPadw5EuQfQJ21a4dmaMbSDu1u6aPoKHsQ
zF/DnySx5ZEdjNz9xMvz7g8LoRAWR4dMsuZFOBismy7KmMqFNsw0GNSRcDT5ffZf
MmXbI654Jv4s6UMl2ghyK7xmYiEeSeVH41AF/LaZMl4V5KSNKuOhn764DzB6z0d4
1XQrq3wCpJ3K4hknKZhTbgSAmvk7i8CwpWZfUWAzuBL9jS1Z6RoN/zgsjM5vDMSs
QUEguBjytPKioiuhZh+F+buvoNDKWvYwlJk8tUyNwtFOhWjniend7zEeioVO/kdg
abSrBmjqZkxbCVreNdDekiGwOz1iD1dlreW6lJ6hW0mBnSAafR/lwXLJCE1slTWf
LIbM8DLZMMeKZ9v7M+VMlW0uL36utP2hMnzHnh9f9eYG
-----END ENCRYPTED PRIVATE KEY-----
@@ -0,0 +1 @@
CWokJ23olHitbyjqw6aZoIFhAo3mwzwdEEkv7sjh600hPwR0YNUDxSkNA7ztK5Ii
Binary file not shown.
+29
View File
@@ -0,0 +1,29 @@
-----BEGIN CERTIFICATE-----
MIIE6jCCAtKgAwIBAgIIG/hHrySZ878wDQYJKoZIhvcNAQELBQAwEjEQMA4GA1UE
AwwHYXZicm9vdDAgFw0yMzEyMjYwMzQwNDBaGA8yMDUxMDUxMzAzNDA0MFowEjEQ
MA4GA1UEAwwHYXZicm9vdDCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIB
AK9VVEeI2UnjQr3IrVlZddq2lTHOlV8qFBVzMguVJUr128HqIXQRgXbEzHfCOOzC
Nuh2x+cBXO7gztpwjSJFi2P+q4OdEujmgCf8SdwxS1NqWo5TRsB9szPALeQ+FQi8
yir+urv6uJUuDpwwPgjhCDwn1tL426c4HgGsQpO0IWjCzc2SWL+KP4zirk7GIf2R
Fy0TsFeJi5ucHWiMuXCGyiPZ2xV7AZ3AakZnf6rqWEIkOOsBf6UVNxFKuppMBcY6
Lb6YcXB7nn2xFwZ+S5gGdZjkZIlLAoZjRynVRBz2BwlJB6ignH8+MbWkYS6ls9C8
C10SmiGXI/S/DhRR11JCNAC5AD8FO+A6i9Vp19PiVmUv6nTx01+FhRqTeiW5bPhk
PTn9gUAU5FKL4ouhr3ojJd6BczdJ2lWB9lQYmWnwzdYnSAnvXwc3WaSX+ryi7LoJ
sZtlc91ZT2yNiqmPQpH6fcPKfi/BJZhe61hZ7Xv3+OVupbmU+IuUK9iKEw5+aqNt
DiT15PKoyKuAfm0A/TJ1UV/P4FbSavEOrMse2SZvhRL/EvwpRXwaiwPFQL/vZ5GZ
mGCXdmVt0FEH9z3UyYq9+UIhZoQh3V9eSrOgjOh3dnFZN5E/UQzqndykGy+pYjG4
e4ZZQjQR5G3pmAiCIngqnqRUNWpG5DQcZYuXLdor19o5AgMBAAGjQjBAMB0GA1Ud
DgQWBBQgy5IQsP4+QnQRWlMJiFsqwyXAzTAPBgNVHRMBAf8EBTADAQH/MA4GA1Ud
DwEB/wQEAwIBBjANBgkqhkiG9w0BAQsFAAOCAgEAn8pexl/crAWBPJ/nPu9rhRo0
dowLhbamhx/i5RRNxIJl8Lpf78Boj4t9ICuTU4yeLFHfA+IcnthS/wrFADbhSgF5
m4gLBSfRjtiOPJSJqkIqCIV6RIcbfdCJa/V3p1nvu3vuGYEp9eSYACEtv6kLaMFu
61RsEWgR6tNmb1Hssl9XzGdM2yf2vy/Gip+Ugztz9gfTF3Vhdos9VToPOzLNQh6C
dr+uLKnnPHOb9PfAonYs23MAfpBaP8m4sEMkyMgACCUP+qld0S8xpptEA/9Wblxy
zWJIZTNWhyfo7cH030lVbdgMBYtZN/WxYVtD3fp+BZPk5fD3QwM712Ja288anDic
HRwtc0wFwJ9EOOwXT1kYT1GgKrmLTnmAGDktnjKtsHMvl7Rn2Wf+5XTQ3FC99DJU
q/Jhe3BGUvOXS7uPfUSMQrH4q+cwUdPSLGu2TSPyXVUEv5/Z4mH4JD+HQbJrKjdO
NvUlnNJgtL3nIW8iSR1BRkIQn9HAnHtfYELHFBr1XHdn8LCjeWmYStdI3s9jucWb
5uIfij1/50hc6A5zl20/y1h9OGFKdRttjk9XzRR6HPt22zyStjab5Y5SzHOwMCaP
wk/uDc+Aestyqm7BoIqU8XptC+VV92OoNiEouI0UI5zPw9EWLRmTjFAc4NzOc1I7
2AKjtz3xnKC3R3oqtsA=
-----END CERTIFICATE-----
+54
View File
@@ -0,0 +1,54 @@
-----BEGIN ENCRYPTED PRIVATE KEY-----
MIIJrTBXBgkqhkiG9w0BBQ0wSjApBgkrBgEEAdpHBAswHAQQ1ucIEwTFlt1U7QDz
w8o/RAICQAACAQgCAQEwHQYJYIZIAWUDBAEqBBAqttk+qw4JJKTbU1zgKChgBIIJ
ULyEdMcpMWYvgN6aefcIns6UJr3bYfq8aX0FvhGXzO1N8Voxg/jKc753Hj3F+h1d
xfJyFzpg3D2jIIR3GRZrXILqc+aXaW3AS8/U7IN2d4gJIrYAbUelBnssdgLg6FSY
MUprWGUC8ShMZpOhAoDD2V8ZHyb9NHJ/xPpbhhBUvDl/jQWurCo3KnWlFiXiGO3I
VyH09LL0T+v4OcU7HjrVHWXokcv+6mkcPvkZZI6MTMC+7UyxNi9WlZs98Lzckpax
asiEHImnDbtGsvYXvjKWn7QXs1CF4SVZ5DdoHMtf1BEHMnXTQ4teSRkp8B02ihr6
LtSoStVXdw1E3+5R6DmW8b+Bxi7duO9URazkH2dHG7ZJvhL86DcvsFNnMW7NZ3PE
N0BYDGTPjSAp8qbkCesbzhYEoDFsTfp6S1vLB+9PL0/S8jJ76MgZJ5f6i69aEz62
Ek7pbydMnrMlMoWmgNEzcywGf1GnNcSQfPUVUyW11HLShNUoD+PktrnPRHhti2cr
VXfWNeaf9lEwnQQYPGEuwxx4+lfK/sL6TV245ziVFAWQ0koJHIF09nIlFKGT9sOa
JrwXWSMg1iT2iua4OLvwsHs7oMMvC+C4ycEMS0NKPEVx3pyAPrsO+rAHnEeUIz/D
ooZEjnSfE9Xlsb5W4qqPuso4H9s/ulFsGz312ZEnt3EDGrgW2BksG42F7G3Qm8nn
wUzSEGAAs/iMO/qbVF6P5/LnoO7h+ds9GCudmBYPKj1o5cwLcyNPjBHbKAXlzvOJ
xyx17aCJ2f7uZzto1ov05lr6N+0/ajRwQn9IIx3R1wFvssETav+fXLavvm0Ul065
iQgpyQnmZeKgzXClwmc9HgoFIMQSzs3lfRgV2coQCvFMgKvP6cVLzMf98NUf7XmM
/fL6aG62KGUVSaat+4mfPT8p8MiuGBz2BmTkEtTWMf+HsgPu3yYKHJ6iAF4JjZ2m
xpDOAd2jACpVVQ+7fxyejSl/VILFuJPL7RKAvIjJzq40X3o/w2Ydih1LgGLiyFHe
I9RXAqCkrA7rbN/TEtZkgpgsLUbbY2b5+rd++DoCIzB4zF6JSE1RTRYkR4jsxyFB
BFwNqgZX/xpA0vQ4WqpYkAc4tsefJEXxt2SsI4C3pVYLYuso+4hY0ZP+n3QXseyV
N6VKpbztWleHXeLI53Bmb5kj4oJk44ouO36A/DtUlEESfvtGrnqRzVG8vMIqhOOU
sX8L+XmvCSnCkV8zDqaI/Wmr/X+4BQMjWJ3C0v3RBXJNCPWaQG9laNoHrcdCxGuR
GCNArOhyxHfjl7jplw2YgNscPPd/GFuUOY3UEG7hWW14/cpGBz5D/PfKnq1QkKw+
e5xvq/+BYGch3K0kOh3aEhbgPZxztIF+JZMcSokNDMPATE7zPpCxAYFCkasA/ccJ
CTeyZqvLGMFYuMwe/rnPbgcxr/1fEEzgAsYkC6JqsIFq2uhEaDPrvmQ9LwAAwBfw
EWhHpSOtMxEwOvzOqGuKDqUy918f5xSXyvKGPA8RC2ZPEEVHChQfyRRcS/9TRqhR
6E0h6frMunWUdBtiDmm3iC6hllWVmwmXJL68aOWokCOf0hBKLl0tGw2QKsaeD3ta
SV8478YUoPn2oNZSQW4QwncQMU/6djjtsL31MF6itZU3ethXbxOQWO3RkybFY1iL
2eZmExFd9IdagxrI8xOxxMq7hYbAF+gB1vgcuzktz3/3L/lBAAl6n+9eAAK3E87U
jhNDC/flJWxpdbNmwGoAUmGMt+oemdtE2gqDpCMgU/TEpuJcPrNYf3cce/CFIYOG
tsYYPzp1QE63418/A7+9nJYTC2Bqqc0nLnARVXKMUpAUuf5xnRNvMod50xMZviJi
aMdGcKnMVAk0esJG68/W3cQdK1efIeIhd7pFYUq1WdzBwfJV3BUYbXLV40qld50U
EuxUPxWVCKD0soPvRl6DqLY7fE4gzEykwzjc2Y6l8oLuQfAzaNX/ItEVX+qvUXKe
myZpKYqAqjOOdAZHLfyyFQzIMHzls83XhTMixlY53ZNMn3rs34LYR0F2k7is6Ysh
qxulqwQ/EcY446WQyTw3XT0IPLIIBXQGpYsuYChn/U2QVSoJxSS1LIeOpMSNJOwk
QnsPtxB6gK3Svt/wdnK0GCZEKwMBx9VAz+M1NKCQQMsv2xWdQ4qm6fOhMEdm/oq5
Pc9pQrq2J9XwBI/3AncKvjxKCfRC6Ob6gEO4cUybbEAZ9r3GCGtdf2+9x/aJm5XP
OeXXRYbwOQtaQp5GgAHE/WoGPWt+lXKLu5mUqrbONS6TLibdAYve9l4cFp3hTvki
159bCm4HrCUj3Etu3x6Nv5SMhXhRdyULrOvx9GA7kco2arXcxSPM79KiyCPMVvyu
e/sRn3X/dS1NfaJ5IlKYLjiJQVySJOmyNhmlMQkLErqjQ1M2ibwybFglrUdJflcd
Bjk3ZbN0r/BqbWWrwVIUVkq6vr0Yuxb1AgEl6HH1QNcX67XLSrKFG5sGkfsImMTd
gvEYKFukgXmqKX3/p9czjJMYlC6sFT0L3e5G3LiEJlGT5jxklEitcAvwTowhbhCy
T0Jf/KTNn2GnisDUK2DZKRbhiAHv3AEkxAIGg6On5SJ0PzFF0MhtxJ7nA3TqzvUN
gN7jAAftaiALPImQf1EDgj3rHu8O8XAC/Nac65Qvn2ZUjMOW+Cba1POal0G76XZx
JunWIDu0BgOXNc7kBRmE+dK9ppEbD5DScUjosAvkZuF94h23Ofzwm9/xBgXPbaV4
CbWBtvRLuKacuLKF3WfQCw/Wi0K70dwDUEAaM1/rJiavGzkYp1z0KPey3hKDzkFL
2ZjkzQdCreXo48Jwfwu4OQFTpkACWLvB0pLbhBXjq5Q9NEJHizRr08YT0gl+5mbJ
Dk+Aw/n2Wy5ZRVbxxklO5brLnu1R8Rq3Szq1ATENrXSHwIN3sUiOEuNu8tSa3If3
Hb5WS1WELgVPzGaUWKsWI9sooTJEnPOXxBq0U0iZpqpnoUw6NenIgIfBWrvG/7LC
5SPksOVuaD6YdC4OF35/Jlrk4uKnCsW5SYa69Kr5RST/8MFGrefb7233iiRAo3La
vr+uvh4NGA9Jf7Pv4yuN2IuCsyAl2jleSDEj6cf/tTz6m9xPT3Nr1/pKrL8mkRxv
HDHq2ivR6QjBpEEoHITMaYndgOvwpCA1qXdsP/DiugCx
-----END ENCRYPTED PRIVATE KEY-----
@@ -0,0 +1 @@
R9iItBaS278KvqlLyil1yUlizNGvWI93SERQ6uAitbzXm4fOc9t3c8dG8r2ThuZz
+48 -127
View File
@@ -3,44 +3,22 @@
* SPDX-License-Identifier: GPL-3.0-only
*/
use std::{ffi::OsString, path::PathBuf, str::FromStr};
use std::{ffi::OsString, path::PathBuf};
use anyhow::Result;
use clap::{Args, Parser, Subcommand};
use avbroot::cli::args::{LogFormat, LogLevel};
use clap::{Args, Parser, Subcommand, ValueEnum};
#[derive(Debug, Args)]
pub struct DeviceGroup {
/// Device config name.
pub struct ProfileGroup {
/// OTA profile name.
#[arg(short, long, value_name = "NAME")]
pub device: Vec<String>,
pub profile: Vec<String>,
/// All device configs.
#[arg(short, long, conflicts_with = "device")]
/// All profiles.
#[arg(short, long, conflicts_with = "profile")]
pub all: bool,
}
#[derive(Debug, Args)]
pub struct DownloadGroup {
/// Revalidate hash of existing download.
#[arg(long)]
pub revalidate: bool,
/// Download the stripped OTA instead of the full OTA.
#[arg(long)]
pub stripped: bool,
}
#[derive(Debug, Args)]
pub struct PatchGroup {
/// Delete patched output files on success.
#[arg(long)]
pub delete_on_success: bool,
/// Suffix for patched output files.
#[arg(long = "output-file-suffix", value_parser, default_value = ".patched")]
pub suffix: OsString,
}
#[derive(Debug, Args)]
pub struct ConfigGroup {
/// Path to config file.
@@ -53,112 +31,26 @@ pub struct ConfigGroup {
)]
pub config: PathBuf,
/// Working directory for storing images.
#[arg(
short,
long,
value_name = "DIRECTORY",
value_parser,
default_value = "files"
)]
pub work_dir: PathBuf,
}
/// Convert a full OTA to stripped form.
///
/// A stripped OTA omits byte regions of the OTA that aren't needed for testing
/// avbroot's patching logic (eg. the system partition image). This reduces the
/// size of the test files by about two orders of magnitude.
#[derive(Debug, Parser)]
pub struct StripCli {
/// Path to original OTA zip.
#[arg(short, long, value_name = "FILE", value_parser)]
pub input: PathBuf,
/// Path to new stripped OTA zip.
#[arg(short, long, value_name = "FILE", value_parser)]
pub output: PathBuf,
}
#[derive(Debug, Clone)]
pub struct Sha256Arg(pub [u8; 32]);
impl FromStr for Sha256Arg {
type Err = hex::FromHexError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let mut data = [0u8; 32];
hex::decode_to_slice(s, &mut data)?;
Ok(Self(data))
}
}
/// Add a new OTA image to the test config.
///
/// This will download the OTA image, strip it, patch both images, and add the
/// resulting metadata (eg. checksums) to the specified test config file.
#[derive(Debug, Parser)]
pub struct AddCli {
/// URL to the full OTA zip.
#[arg(short, long)]
pub url: String,
/// Device config name.
#[arg(short, long, value_name = "NAME")]
pub device: String,
/// Expected sha256 hash of the full OTA zip.
#[arg(short = 'H', long, value_name = "SHA256_HEX", value_parser)]
pub hash: Option<Sha256Arg>,
#[command(flatten)]
pub patch: PatchGroup,
#[command(flatten)]
pub config: ConfigGroup,
/// Skip verifying OTA and AVB signatures.
/// Working directory.
///
/// OTAs for some devices (eg. ossi) ship with vbmeta partitions containing
/// invalid hashes. These will normally fail during validation.
#[arg(long)]
pub skip_verify: bool,
}
/// Download a device image.
#[derive(Debug, Parser)]
pub struct DownloadCli {
/// Download the Magisk APK.
#[arg(short, long)]
pub magisk: bool,
#[command(flatten)]
pub device: DeviceGroup,
#[command(flatten)]
pub download: DownloadGroup,
#[command(flatten)]
pub config: ConfigGroup,
/// If unset, a temporary directory is used, which will be automatically
/// cleaned up, even if a failure occurs. Custom working directories are
/// not deleted.
#[arg(short, long, value_name = "DIRECTORY", value_parser)]
pub work_dir: Option<PathBuf>,
}
/// Run tests.
#[derive(Debug, Parser)]
pub struct TestCli {
#[command(flatten)]
pub device: DeviceGroup,
#[command(flatten)]
pub download: DownloadGroup,
#[command(flatten)]
pub patch: PatchGroup,
pub profile: ProfileGroup,
#[command(flatten)]
pub config: ConfigGroup,
}
/// List devices in config file.
/// List profiles in config file.
#[derive(Debug, Parser)]
pub struct ListCli {
#[command(flatten)]
@@ -167,9 +59,6 @@ pub struct ListCli {
#[derive(Debug, Subcommand)]
pub enum Command {
Strip(StripCli),
Add(AddCli),
Download(DownloadCli),
Test(TestCli),
List(ListCli),
}
@@ -178,4 +67,36 @@ pub enum Command {
pub struct Cli {
#[command(subcommand)]
pub command: Command,
/// Lowest log message severity to output.
#[arg(long, global = true, value_name = "LEVEL", default_value_t)]
pub log_level: LogLevel,
/// Output format for log messages.
#[arg(long, global = true, value_name = "FORMAT", default_value_t = LogFormat::Medium)]
pub log_format: LogFormat,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, ValueEnum)]
pub enum PassSource {
Env,
File,
}
#[derive(Debug, Parser)]
pub struct HelperCli {
/// Signature algorithm.
pub algorithm: String,
/// Public key.
#[arg(value_name = "FILE", value_parser)]
pub public_key: PathBuf,
/// Non-interactive password source.
#[arg(value_name = "SOURCE")]
pub pass_source: PassSource,
/// Non-interactive password source value.
#[arg(value_name = "VALUE", value_parser)]
pub pass_source_value: OsString,
}
+92 -100
View File
@@ -1,17 +1,13 @@
/*
* SPDX-FileCopyrightText: 2023 Andrew Gunnerson
* SPDX-FileCopyrightText: 2023-2024 Andrew Gunnerson
* SPDX-License-Identifier: GPL-3.0-only
*/
use std::{collections::BTreeMap, fs, ops::Range, path::Path};
use std::{collections::BTreeMap, fs, path::Path};
use anyhow::{Context, Result};
use serde::{Deserialize, Serialize};
use toml_edit::{
ser::ValueSerializer,
visit_mut::{self, VisitMut},
Array, Document, InlineTable, Item, KeyMut, Table, Value,
};
use toml_edit::DocumentMut;
#[derive(Serialize, Deserialize)]
pub struct Sha256Hash(
@@ -23,118 +19,114 @@ pub struct Sha256Hash(
);
#[derive(Serialize, Deserialize)]
pub struct Magisk {
pub url: String,
pub hash: Sha256Hash,
#[serde(deny_unknown_fields)]
pub struct OtaInfo {
pub device: String,
pub fingerprint: String,
pub build_number: String,
pub incremental_version: String,
pub android_version: String,
pub sdk_version: String,
pub security_patch_level: String,
}
#[derive(Serialize, Deserialize)]
pub struct OtaHashes {
pub full: Sha256Hash,
pub stripped: Sha256Hash,
#[serde(deny_unknown_fields)]
pub struct Avb {
pub signed: bool,
}
#[derive(Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum RamdiskContent {
Init,
Otacerts,
FirstStage,
DsuKeyDir,
Dlkm,
}
#[derive(Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum BootVersion {
V2,
V3,
V4,
VendorV3,
VendorV4,
}
#[derive(Serialize, Deserialize)]
pub struct ImageHashes {
pub original: OtaHashes,
pub patched: OtaHashes,
pub avb_images: BTreeMap<String, Sha256Hash>,
#[serde(deny_unknown_fields)]
pub struct BootData {
pub version: BootVersion,
#[serde(default)]
pub kernel: bool,
#[serde(default)]
pub ramdisks: Vec<Vec<RamdiskContent>>,
}
#[derive(Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum DmVerityContent {
SystemOtacerts,
}
#[derive(Serialize, Deserialize)]
pub struct Device {
pub url: String,
pub sections: Vec<Range<u64>>,
pub hash: ImageHashes,
#[serde(deny_unknown_fields)]
pub struct DmVerityData {
pub content: DmVerityContent,
}
#[derive(Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct VbmetaData {
pub deps: Vec<String>,
}
#[derive(Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum Data {
Boot(BootData),
DmVerity(DmVerityData),
Vbmeta(VbmetaData),
}
#[derive(Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Hashes {
pub original: Sha256Hash,
pub patched: Sha256Hash,
}
#[derive(Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Partition {
pub avb: Avb,
pub data: Data,
}
#[derive(Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Profile {
pub partitions: BTreeMap<String, Partition>,
pub hashes: Hashes,
}
#[derive(Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Config {
pub magisk: Magisk,
pub device: BTreeMap<String, Device>,
pub ota_info: OtaInfo,
#[serde(default)]
pub profile: BTreeMap<String, Profile>,
}
struct ConfigFormatter;
impl VisitMut for ConfigFormatter {
fn visit_table_like_kv_mut(&mut self, key: KeyMut<'_>, node: &mut Item) {
// Convert non-array-of-tables inline tables into regular tables.
if let Item::Value(Value::InlineTable(t)) = node {
let inline_table = std::mem::replace(t, InlineTable::new());
*node = Item::Table(inline_table.into_table());
}
// But for hashes, use dotted notation until TOML 1.1, which allows
// newlines in inline tables, is released.
if key == "hash" || key == "original" || key == "patched" || key == "avb_images" {
if let Some(t) = node.as_table_like_mut() {
t.set_dotted(true);
}
}
visit_mut::visit_table_like_kv_mut(self, key, node);
}
fn visit_table_mut(&mut self, node: &mut Table) {
// Make tables implicit unless they are empty, which may be meaningful.
if !node.is_empty() {
node.set_implicit(true);
}
visit_mut::visit_table_mut(self, node);
}
fn visit_array_mut(&mut self, node: &mut Array) {
visit_mut::visit_array_mut(self, node);
// Put array elements on their own indented lines.
if node.is_empty() {
node.set_trailing("");
node.set_trailing_comma(false);
} else {
for item in node.iter_mut() {
item.decor_mut().set_prefix("\n ");
}
node.set_trailing("\n");
node.set_trailing_comma(true);
}
}
}
/// Add a device to the config file. This leaves all comments intact, except for
/// those contained within the existing device section if it exists.
pub fn add_device(document: &mut Document, name: &str, device: &Device) -> Result<()> {
let device_table = document.entry("device").or_insert_with(|| {
let mut t = toml_edit::Table::new();
t.set_implicit(true);
Item::Table(t)
});
let old_table = device_table.get(name).and_then(|i| i.as_table());
let value = device.serialize(ValueSerializer::new())?;
let Value::InlineTable(inline_table) = value else {
unreachable!("Device did not serialize as an inline table");
};
let mut table = inline_table.into_table();
ConfigFormatter.visit_table_mut(&mut table);
// Keep top-level comment on the table.
if let Some(t) = old_table {
*table.decor_mut() = t.decor().clone();
}
device_table[name] = Item::Table(table);
Ok(())
}
pub fn load_config(path: &Path) -> Result<(Config, Document)> {
pub fn load_config(path: &Path) -> Result<(Config, DocumentMut)> {
let contents =
fs::read_to_string(path).with_context(|| format!("Failed to read config: {path:?}"))?;
let config: Config = toml_edit::de::from_str(&contents)
.with_context(|| format!("Failed to parse config: {path:?}"))?;
let document: Document = contents.parse().unwrap();
let document: DocumentMut = contents.parse().unwrap();
Ok((config, document))
}
-462
View File
@@ -1,462 +0,0 @@
/*
* SPDX-FileCopyrightText: 2023 Andrew Gunnerson
* SPDX-License-Identifier: GPL-3.0-only
*/
use std::{
collections::{HashMap, VecDeque},
fs::{self, OpenOptions},
io::{self, Read, Seek, SeekFrom, Write},
ops::Range,
path::{Path, PathBuf},
sync::{
atomic::{AtomicBool, Ordering},
mpsc::{self, Sender},
},
thread::{self, ThreadId},
time::{Duration, Instant},
};
use anyhow::{anyhow, bail, Context, Result};
use avbroot::stream::{PSeekFile, Reopen};
use serde::{Deserialize, Serialize};
/// Minimum download chunk size per task.
const MIN_CHUNK_SIZE: u64 = 1024 * 1024;
const TIMEOUT: Duration = Duration::from_secs(5);
pub trait ProgressDisplay {
fn progress(&mut self, current: u64, total: u64);
fn error(&mut self, msg: &str);
fn finish(&mut self);
}
pub struct BasicProgressDisplay {
current: u64,
total: u64,
interval: Duration,
last_render: Instant,
avg: VecDeque<(Instant, u64)>,
}
// Speed is a simple moving average over 5 seconds.
static AVG_INTERVAL: Duration = Duration::from_millis(100);
static AVG_WINDOW_SIZE: usize = 5000 / AVG_INTERVAL.as_millis() as usize;
impl BasicProgressDisplay {
pub fn new(interval: Duration) -> Self {
Self {
current: 0,
total: 0,
interval,
last_render: Instant::now() - interval,
avg: VecDeque::new(),
}
}
fn clear_line(&self) {
eprint!("\x1b[2K\r");
}
}
impl ProgressDisplay for BasicProgressDisplay {
fn progress(&mut self, current: u64, total: u64) {
self.current = current;
self.total = total;
let now = Instant::now();
if self.avg.is_empty() || (now - self.avg.back().unwrap().0) > AVG_INTERVAL {
if self.avg.len() == AVG_WINDOW_SIZE {
self.avg.pop_front();
}
self.avg.push_back((now, current));
}
if now - self.last_render > self.interval {
let current_mib = current as f64 / 1024.0 / 1024.0;
let total_mib = total as f64 / 1024.0 / 1024.0;
let front = self.avg.front().unwrap();
let back = self.avg.back().unwrap();
let avg_window_mib = (back.1 - front.1) as f64 / 1024.0 / 1024.0;
let avg_window_duration = back.0 - front.0;
let speed_mib_s = if avg_window_duration.is_zero() {
0.0
} else {
avg_window_mib / avg_window_duration.as_secs_f64()
};
self.clear_line();
eprint!("{current_mib:.1} / {total_mib:.1} MiB ({speed_mib_s:.1} MiB/s)");
self.last_render = now;
}
}
fn error(&mut self, msg: &str) {
self.clear_line();
eprintln!("{msg}");
}
fn finish(&mut self) {
self.clear_line();
}
}
#[derive(Debug)]
enum MessageData {
Progress {
bytes: u64,
// Controller replies with a new ending offset.
resp: Sender<u64>,
},
Completion {
result: Result<()>,
},
}
#[derive(Debug)]
struct Message {
id: ThreadId,
data: MessageData,
}
/// Download a contiguous byte range. The number of bytes downloaded per loop
/// iteration will be sent to the specified channel via a `ProgressMessage`. The
/// receiver of the message must reply with the new ending offset for this
/// download via the oneshot channel in the `resp` field. An appropriate error
/// will be returned if the full range (subject to modification) cannot be fully
/// downloaded (eg. premature EOF is an error).
fn download_range(
url: &str,
mut file: PSeekFile,
initial_range: Range<u64>,
channel: Sender<Message>,
cancel_signal: &AtomicBool,
) -> Result<()> {
assert!(initial_range.start < initial_range.end);
let mut response = attohttpc::get(url)
.connect_timeout(TIMEOUT)
.read_timeout(TIMEOUT)
.header(
"Range",
&format!("bytes={}-{}", initial_range.start, initial_range.end - 1),
)
.send()
.and_then(|r| r.error_for_status())
.with_context(|| format!("Failed to start download for range: {initial_range:?}"))?;
let mut range = initial_range.clone();
let mut buf = [0u8; 65536];
while range.start < range.end {
if cancel_signal.load(Ordering::SeqCst) {
bail!("Received cancel signal");
}
let to_read = (range.end - range.start).min(buf.len() as u64) as usize;
let n = response.read(&mut buf[..to_read]).with_context(|| {
format!(
"Failed to download {to_read} bytes at offset {}",
range.start,
)
})?;
if n == 0 {
bail!("Unexpected EOF from server");
}
// This may overlap with another task's write when a range split occurs,
// but the same data will be written anyway, so it's not a huge deal.
file.seek(SeekFrom::Start(range.start))?;
file.write_all(&buf[..n]).with_context(|| {
format!(
"Failed to write {n} bytes to output file at offset {}",
range.start,
)
})?;
range.start += n as u64;
// Report progress to the controller.
let (tx, rx) = mpsc::channel();
let msg = Message {
id: thread::current().id(),
data: MessageData::Progress {
bytes: n as u64,
resp: tx,
},
};
channel.send(msg)?;
// Get new ending offset from controller.
let new_end = rx.recv()?;
if new_end != range.end {
debug_assert!(new_end <= range.end);
range.end = new_end;
}
}
Ok(())
}
/// This just calls [`download_range()`] and sends a completion message to the
/// channel with the result.
fn download_thread(
url: &str,
file: PSeekFile,
initial_range: Range<u64>,
channel: mpsc::Sender<Message>,
cancel_signal: &AtomicBool,
) {
let result = download_range(url, file, initial_range, channel.clone(), cancel_signal);
channel
.send(Message {
id: thread::current().id(),
data: MessageData::Completion { result },
})
.unwrap();
}
/// Send a HEAD request to get the value of the Content-Length header.
fn get_content_length(url: &str) -> Result<u64> {
let response = attohttpc::head(url)
.connect_timeout(TIMEOUT)
.read_timeout(TIMEOUT)
.send()
.and_then(|r| r.error_for_status())
.context("Failed to send HEAD request to get Content-Length")?;
response
.headers()
.get("Content-Length")
.and_then(|h| h.to_str().ok())
.and_then(|h| h.parse().ok())
.ok_or_else(|| anyhow!("HEAD request did not return a valid Content-Length"))
}
/// Download a set of file chunks in parallel. Only unrecoverable errors are
/// returned as an Err. Normal/expected errors and download progress info are
/// reported via `display`. Returns the remaining ranges that need to be
/// downloaded.
fn download_ranges(
url: &str,
output: &Path,
initial_ranges: Option<&[Range<u64>]>,
display: &mut dyn ProgressDisplay,
max_threads: usize,
max_errors: u8,
cancel_signal: &AtomicBool,
) -> Result<Vec<Range<u64>>> {
let file_size = get_content_length(url)?;
// Open for writing, but without truncation.
let file = OpenOptions::new()
.write(true)
.create(true)
.open(output)
.map(PSeekFile::new)
.with_context(|| format!("Failed to open for writing: {output:?}"))?;
file.set_len(file_size)
.with_context(|| format!("Failed to set file size: {output:?}"))?;
// Queue of ranges that need to be downloaded.
let mut remaining = VecDeque::from(match initial_ranges {
Some(r) => r.to_vec(),
#[allow(clippy::single_range_in_vec_init)]
None => vec![0..file_size],
});
// Ranges that have failed.
let mut failed = Vec::<Range<u64>>::new();
// Ranges for currently running threads.
let mut thread_ranges = HashMap::<ThreadId, Range<u64>>::new();
// Overall progress.
let mut progress = file_size - remaining.iter().map(|r| r.end - r.start).sum::<u64>();
display.progress(progress, file_size);
thread::scope(|scope| {
let mut threads = HashMap::new();
let mut error_count = 0u8;
// Progress messages from threads.
let (tx, rx) = mpsc::channel();
loop {
// Spawn new threads.
while !cancel_signal.load(Ordering::SeqCst) && threads.len() < max_threads {
if remaining.is_empty() && !threads.is_empty() {
// No more ranges to download. Split another thread's range.
let (_, old_range) = thread_ranges
.iter_mut()
.max_by_key(|(_, r)| r.end - r.start)
.unwrap();
let size = old_range.end - old_range.start;
if size >= MIN_CHUNK_SIZE {
let new_range = old_range.start + size / 2..old_range.end;
old_range.end = new_range.start;
remaining.push_back(new_range);
}
}
if let Some(thread_range) = remaining.pop_front() {
// PSeekFile's reopen can't fail.
let file_cloned = file.reopen().unwrap();
let thread_range_cloned = thread_range.clone();
let tx_cloned = tx.clone();
let join_handle = scope.spawn(|| {
download_thread(
url,
file_cloned,
thread_range_cloned,
tx_cloned,
cancel_signal,
)
});
thread_ranges.insert(join_handle.thread().id(), thread_range);
threads.insert(join_handle.thread().id(), join_handle);
} else {
// No pending ranges and no running threads can be split.
break;
}
}
if threads.is_empty() {
// Nothing left to do.
break;
}
let Message { id, data } = rx.recv().unwrap();
match data {
MessageData::Progress { bytes, resp } => {
progress += bytes;
display.progress(progress, file_size);
let thread_range = thread_ranges.get_mut(&id).unwrap();
thread_range.start += bytes;
resp.send(thread_range.end).unwrap();
}
MessageData::Completion { result } => {
threads.remove(&id).unwrap().join().unwrap();
let thread_range = thread_ranges.remove(&id).unwrap();
if let Err(e) = result {
display.error(&format!("[{id:?}] {e:?}"));
error_count += 1;
if error_count < max_errors {
remaining.push_back(thread_range);
} else {
failed.push(thread_range);
}
}
}
}
}
});
display.finish();
failed.extend(remaining);
failed.extend(thread_ranges.into_values());
Ok(failed)
}
#[derive(Serialize, Deserialize)]
struct State {
ranges: Vec<Range<u64>>,
}
fn read_state(path: &Path) -> Result<Option<State>> {
let data = match fs::read_to_string(path) {
Ok(f) => f,
Err(e) if e.kind() == io::ErrorKind::NotFound => return Ok(None),
Err(e) => Err(e).with_context(|| format!("Failed to read download state: {path:?}"))?,
};
let state = toml_edit::de::from_str(&data)
.with_context(|| format!("Failed to parse download state: {path:?}"))?;
Ok(Some(state))
}
fn write_state(path: &Path, state: &State) -> Result<()> {
let data = toml_edit::ser::to_string(state).unwrap();
fs::write(path, data).with_context(|| format!("Failed to write download state: {path:?}"))?;
Ok(())
}
fn delete_if_exists(path: &Path) -> Result<()> {
if let Err(e) = fs::remove_file(path) {
if e.kind() != io::ErrorKind::NotFound {
return Err(e).context(format!("Failed to delete file: {path:?}"));
}
}
Ok(())
}
pub fn state_path(path: &Path) -> PathBuf {
let mut s = path.as_os_str().to_owned();
s.push(".state");
PathBuf::from(s)
}
/// Download `url` to `output` with parallel threads.
///
/// If `initial_ranges` is specified, only those sections of the file will be
/// downloaded. The empty regions are left untouched (i.e. filled with zeroes).
/// A `.state` file is written if the download is interrupted. If the state
/// file exists when this function is called, `initial_ranges` is ignored and
/// the ranges from the state file are used to resume the download.
pub fn download(
url: &str,
output: &Path,
initial_ranges: Option<&[Range<u64>]>,
display: &mut dyn ProgressDisplay,
max_tasks: usize,
max_errors: u8,
cancel_signal: &AtomicBool,
) -> Result<()> {
let state_path = state_path(output);
let ranges = match read_state(&state_path)? {
Some(r) => Some(r.ranges),
None => initial_ranges.map(|r| r.to_vec()),
};
let remaining = download_ranges(
url,
output,
ranges.as_deref(),
display,
max_tasks,
max_errors,
cancel_signal,
)?;
if remaining.is_empty() {
delete_if_exists(&state_path)?;
} else {
write_state(&state_path, &State { ranges: remaining })?;
bail!("Download was interrupted");
}
Ok(())
}
+1143 -600
View File
File diff suppressed because it is too large Load Diff
-6
View File
@@ -1,6 +0,0 @@
id=com.chiller3.avbroot.clearotacerts
name=clearotacerts
version=v2.3.0
versionCode=131840
author=chenxiaolong
description=Block A/B OTAs by clearing verification certificates
-33
View File
@@ -1,33 +0,0 @@
#!/sbin/sh
#################
# Initialization
#################
umask 022
# echo before loading util_functions
ui_print() { echo "$1"; }
require_new_magisk() {
ui_print "*******************************"
ui_print " Please install Magisk v20.4+! "
ui_print "*******************************"
exit 1
}
#########################
# Load util_functions.sh
#########################
OUTFD=$2
ZIPFILE=$3
mount /data 2>/dev/null
[ -f /data/adb/magisk/util_functions.sh ] || require_new_magisk
. /data/adb/magisk/util_functions.sh
[ $MAGISK_VER_CODE -lt 20400 ] && require_new_magisk
install_module
exit 0
-1
View File
@@ -1 +0,0 @@
#MAGISK
-83
View File
@@ -1,83 +0,0 @@
import android.annotation.SuppressLint;
import android.os.IBinder;
import android.os.IInterface;
import android.os.Process;
import android.system.ErrnoException;
import java.lang.reflect.Method;
@SuppressLint({"DiscouragedPrivateApi", "PrivateApi", "SoonBlockedPrivateApi"})
public class Main {
private static final int GET_SERVICE_ATTEMPTS = 30;
@SuppressWarnings("SameParameterValue")
private static IInterface getService(Class<?> interfaceClass, String serviceName) throws Exception {
Class<?> serviceManager = Class.forName("android.os.ServiceManager");
Method getService = serviceManager.getDeclaredMethod("getService", String.class);
Class<?> stub = Class.forName(interfaceClass.getCanonicalName() + "$Stub");
Method asInterface = stub.getDeclaredMethod("asInterface", IBinder.class);
// ServiceManager.waitForService() tries to start the service, which we want to avoid to be
// 100% sure we're not disrupting the boot flow.
for (int attempt = 1; attempt <= GET_SERVICE_ATTEMPTS; ++attempt) {
IBinder iBinder = (IBinder) getService.invoke(null, serviceName);
if (iBinder != null) {
return (IInterface) asInterface.invoke(null, iBinder);
}
if (attempt < GET_SERVICE_ATTEMPTS) {
Thread.sleep(1000);
}
}
throw new IllegalStateException(
"Service " + serviceName + " not found after " + GET_SERVICE_ATTEMPTS + " attempts");
}
@SuppressWarnings("ConstantConditions")
private static void unlock() throws Exception {
Class<?> iOemLockService = Class.forName("android.service.oemlock.IOemLockService");
IInterface iFace = getService(iOemLockService, "oem_lock");
Method setOemUnlockAllowedByUser = iOemLockService.getDeclaredMethod("setOemUnlockAllowedByUser", boolean.class);
Method isOemUnlockAllowedByUser = iOemLockService.getDeclaredMethod("isOemUnlockAllowedByUser");
Boolean unlockAllowed = (Boolean) isOemUnlockAllowedByUser.invoke(iFace);
if (unlockAllowed) {
System.out.println("OEM unlocking already enabled");
return;
}
System.out.println("Enabling OEM unlocking");
setOemUnlockAllowedByUser.invoke(iFace, true);
}
@SuppressWarnings({"ConstantConditions", "JavaReflectionMemberAccess"})
private static void switchToSystemUid() throws Exception {
if (Process.myUid() != Process.SYSTEM_UID) {
Method setUid = Process.class.getDeclaredMethod("setUid", int.class);
int errno = (int) setUid.invoke(null, Process.SYSTEM_UID);
if (errno != 0) {
throw new Exception("Failed to switch to SYSTEM (" + Process.SYSTEM_UID + ") user",
new ErrnoException("setuid", errno));
}
if (Process.myUid() != Process.SYSTEM_UID) {
throw new IllegalStateException("UID didn't actually change: " +
Process.myUid() + " != " + Process.SYSTEM_UID);
}
}
}
public static void main(String[] args) {
try {
switchToSystemUid();
unlock();
} catch (Exception e) {
System.err.println("Failed to enable OEM unlocking");
e.printStackTrace();
System.exit(1);
}
}
}
-6
View File
@@ -1,6 +0,0 @@
id=com.chiller3.avbroot.oemunlockonboot
name=oemunlockonboot
version=v2.3.0
versionCode=131840
author=chenxiaolong
description=Enable OEM unlocking on every boot
-20
View File
@@ -1,20 +0,0 @@
exec >/data/local/tmp/avbroot_oem_unlock.log 2>&1
mod_dir=${0%/*}
header() {
echo "----- ${*} -----"
}
header Environment
echo "Timestamp: $(date)"
echo "Script: ${0}"
echo "UID/GID/Context: $(id)"
header Enable OEM unlocking
CLASSPATH="${mod_dir}/classes.dex" app_process / Main &
pid=${!}
wait "${pid}"
echo "Exit status: ${?}"
echo "Logcat:"
logcat -d --pid "${pid}"
+1 -10
View File
@@ -11,14 +11,5 @@ publish = false
[dependencies]
anyhow = "1.0.75"
clap = { version = "4.4.1", features = ["derive"] }
human-sort = "0.2.2"
regex = { version = "1.9.4", default-features = false, features = ["perf", "std"] }
tempfile = "3.8.0"
toml_edit = "0.20.1"
walkdir = "2.3.3"
# https://github.com/zip-rs/zip/pull/383
[dependencies.zip]
git = "https://github.com/chenxiaolong/zip"
rev = "989101f9384b9e94e36e6e9e0f51908fdf98bde6"
default-features = false
toml_edit = "0.22.9"
+1 -12
View File
@@ -8,7 +8,7 @@ To update the project version, run:
cargo xtask set-version -V <version>
```
This will update the main [`Cargo.toml`](../Cargo.toml) and each module's `module.prop` file.
This will update the main [`Cargo.toml`](../Cargo.toml) file.
## Updating changelog links
@@ -19,14 +19,3 @@ Then, run the following command to generate the appropriate link references:
```bash
cargo xtask update-changelog
```
## Build modules
To build avbroot's companion modules, run:
```bash
cargo xtask modules -a
# or -m <module1> -m <module2> ...
```
See the main [`README.md`](../README.md#avbroot-magisk-modules) for more details.
+1 -4
View File
@@ -4,13 +4,12 @@
*/
mod changelog;
mod module;
mod version;
use anyhow::Result;
use clap::{Parser, Subcommand};
use crate::{module::ModulesCli, version::SetVersionCli};
use crate::version::SetVersionCli;
const WORKSPACE_DIR: &str = env!("CARGO_WORKSPACE_DIR");
@@ -19,7 +18,6 @@ fn main() -> Result<()> {
match cli.command {
Command::SetVersion(c) => version::set_version_subcommand(&c),
Command::Modules(c) => module::modules_subcommand(&c),
Command::UpdateChangelog => changelog::update_changelog_subcommand(),
}
}
@@ -27,7 +25,6 @@ fn main() -> Result<()> {
#[derive(Debug, Subcommand)]
pub enum Command {
SetVersion(SetVersionCli),
Modules(ModulesCli),
/// Update links in CHANGELOG.md.
UpdateChangelog,
}
-242
View File
@@ -1,242 +0,0 @@
/*
* SPDX-FileCopyrightText: 2023 Andrew Gunnerson
* SPDX-License-Identifier: GPL-3.0-only
*/
use std::{
collections::HashMap,
env,
ffi::OsStr,
fmt,
fs::{self, File},
io::{self, BufWriter, Write},
path::{Path, PathBuf},
process::Command,
};
use anyhow::{anyhow, bail, Result};
use clap::{Args, Parser, ValueEnum};
use tempfile::TempDir;
use walkdir::WalkDir;
use zip::{write::FileOptions, ZipWriter};
use crate::WORKSPACE_DIR;
#[cfg(unix)]
const D8: &str = "d8";
#[cfg(windows)]
const D8: &str = "d8.bat";
fn newest_child_by_name(directory: &Path) -> Result<PathBuf> {
let mut children = vec![];
for entry in fs::read_dir(directory)? {
let entry = entry?;
let path = entry
.path()
.into_os_string()
.into_string()
.map_err(|e| anyhow!("Non-UTF-8 path: {e:?}"))?;
children.push(path);
}
children.sort_by(|a, b| human_sort::compare(a, b));
children
.pop()
.map(PathBuf::from)
.ok_or_else(|| anyhow!("{directory:?} has no children"))
}
fn build_empty_zip(writer: &mut dyn Write) -> Result<()> {
let mut writer = ZipWriter::new_streaming(writer);
writer.finish()?;
Ok(())
}
fn build_dex(writer: &mut dyn Write, sources: &[&Path]) -> Result<()> {
let sdk = env::var_os("ANDROID_HOME")
.map(PathBuf::from)
.ok_or_else(|| anyhow!("ANDROID_HOME must be set to the Android SDK path"))?;
let build_tools = newest_child_by_name(&sdk.join("build-tools"))?;
let platform = newest_child_by_name(&sdk.join("platforms"))?;
let d8 = build_tools.join(D8);
let android_jar = platform.join("android.jar");
let temp_dir = TempDir::new()?;
let mut process = Command::new("javac")
.args(["-source", "1.8"])
.args(["-target", "1.8"])
.arg("-cp")
.arg(android_jar)
.arg("-d")
.arg(temp_dir.path())
.args(sources)
.spawn()?;
let status = process.wait()?;
if !status.success() {
bail!("javac failed with {status}");
}
let mut class_files = vec![];
for entry in WalkDir::new(temp_dir.path()) {
let entry = entry?;
if entry.path().extension() == Some(OsStr::new("class")) {
class_files.push(entry.into_path());
}
}
let mut process = Command::new(d8)
.arg("--output")
.arg(temp_dir.path())
.args(class_files)
.spawn()?;
let status = process.wait()?;
if !status.success() {
bail!("d8 failed with {status}");
}
let mut reader = File::open(temp_dir.path().join("classes.dex"))?;
io::copy(&mut reader, writer)?;
Ok(())
}
fn parse_props(data: &str) -> Result<HashMap<String, String>> {
let mut result = HashMap::new();
for line in data.split('\n') {
if line.is_empty() {
continue;
}
let Some((k, v)) = line.split_once('=') else {
bail!("Malformed line: {line:?}");
};
result.insert(k.trim().to_owned(), v.trim().to_owned());
}
Ok(result)
}
fn start_module(
dist_dir: &Path,
common_dir: &Path,
module_dir: &Path,
) -> Result<(PathBuf, ZipWriter<BufWriter<File>>)> {
let module_prop_raw = fs::read_to_string(module_dir.join("module.prop"))?;
let module_prop = parse_props(&module_prop_raw)?;
let name = module_prop["name"].as_str();
let version = module_prop["version"].as_str();
let version = version.strip_prefix('v').unwrap_or(version);
let zip_path = dist_dir.join(format!("{name}-{version}.zip"));
let raw_writer = File::create(&zip_path)?;
let mut zip_writer = ZipWriter::new(BufWriter::new(raw_writer));
zip_writer.start_file(
"META-INF/com/google/android/update-binary",
FileOptions::default(),
)?;
io::copy(
&mut File::open(common_dir.join("update-binary"))?,
&mut zip_writer,
)?;
zip_writer.start_file(
"META-INF/com/google/android/updater-script",
FileOptions::default(),
)?;
io::copy(
&mut File::open(common_dir.join("updater-script"))?,
&mut zip_writer,
)?;
zip_writer.start_file("module.prop", FileOptions::default())?;
zip_writer.write_all(module_prop_raw.as_bytes())?;
Ok((zip_path, zip_writer))
}
pub fn modules_subcommand(cli: &ModulesCli) -> Result<()> {
let modules_dir = Path::new(WORKSPACE_DIR).join("modules");
let common_dir = modules_dir.join("common");
let dist_dir = modules_dir.join("dist");
fs::create_dir_all(&dist_dir)?;
let modules = if cli.module.all {
Module::value_variants()
} else {
&cli.module.module
};
for module in modules {
let module_dir = modules_dir.join(module.to_string());
let (path, mut writer) = start_module(&dist_dir, &common_dir, &module_dir)?;
match module {
Module::ClearOtaCerts => {
writer.start_file("system/etc/security/otacerts.zip", FileOptions::default())?;
build_empty_zip(&mut writer)?;
}
Module::OemUnlockOnBoot => {
writer.start_file("classes.dex", FileOptions::default())?;
build_dex(&mut writer, &[&module_dir.join("Main.java")])?;
writer.start_file("service.sh", FileOptions::default())?;
let mut reader = File::open(module_dir.join("service.sh"))?;
io::copy(&mut reader, &mut writer)?;
}
}
writer.finish()?;
let path = path.canonicalize()?;
println!("Built module: {path:?}");
}
Ok(())
}
#[derive(Clone, Copy, Debug, ValueEnum)]
#[value(rename_all = "lower")]
enum Module {
ClearOtaCerts,
OemUnlockOnBoot,
}
impl fmt::Display for Module {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.to_possible_value()
.expect("no values are skipped")
.get_name()
.fmt(f)
}
}
#[derive(Debug, Args)]
#[group(required = true, multiple = false)]
pub struct ModuleGroup {
/// Name of module.
#[arg(short, long)]
module: Vec<Module>,
/// Build all modules.
#[arg(short, long, conflicts_with = "module")]
all: bool,
}
/// Build companion modules.
#[derive(Debug, Parser)]
pub struct ModulesCli {
#[command(flatten)]
module: ModuleGroup,
}
+4 -55
View File
@@ -1,5 +1,5 @@
/*
* SPDX-FileCopyrightText: 2023 Andrew Gunnerson
* SPDX-FileCopyrightText: 2023-2024 Andrew Gunnerson
* SPDX-License-Identifier: GPL-3.0-only
*/
@@ -11,7 +11,7 @@ use std::{
use anyhow::{bail, Result};
use clap::Parser;
use toml_edit::{value, Document};
use toml_edit::{value, DocumentMut};
use crate::WORKSPACE_DIR;
@@ -19,7 +19,7 @@ fn update_cargo_version(version: &str) -> Result<()> {
let path = Path::new(WORKSPACE_DIR).join("Cargo.toml");
let data = fs::read_to_string(&path)?;
let mut document: Document = data.parse()?;
let mut document: DocumentMut = data.parse()?;
document["workspace"]["package"]["version"] = value(version);
fs::write(path, document.to_string())?;
@@ -27,43 +27,6 @@ fn update_cargo_version(version: &str) -> Result<()> {
Ok(())
}
fn update_module_version(path: &Path, version: &str) -> Result<()> {
let mut version_code = 0;
// 8 bits per version component.
for piece in version.split('.') {
let piece: u32 = piece.parse()?;
version_code <<= 8;
version_code |= piece;
}
let raw_reader = File::open(path)?;
let mut reader = BufReader::new(raw_reader);
let mut result = String::new();
let mut line = String::new();
loop {
line.clear();
let n = reader.read_line(&mut line)?;
if n == 0 {
break;
}
if line.starts_with("version=") {
result.push_str(&format!("version=v{version}\n"));
} else if line.starts_with("versionCode=") {
result.push_str(&format!("versionCode={version_code}\n"));
} else {
result.push_str(&line);
}
}
fs::write(path, &result)?;
Ok(())
}
fn update_changelog_version(version: &str) -> Result<()> {
let path = Path::new(WORKSPACE_DIR).join("CHANGELOG.md");
let raw_reader = File::open(&path)?;
@@ -109,26 +72,12 @@ fn update_changelog_version(version: &str) -> Result<()> {
pub fn set_version_subcommand(cli: &SetVersionCli) -> Result<()> {
update_cargo_version(&cli.version)?;
let modules_dir = Path::new(WORKSPACE_DIR).join("modules");
for entry in fs::read_dir(modules_dir)? {
let entry = entry?;
if entry.file_type()?.is_dir() {
let module_prop = entry.path().join("module.prop");
if module_prop.exists() {
update_module_version(&module_prop, &cli.version)?;
}
}
}
update_changelog_version(&cli.version)?;
Ok(())
}
/// Set the version number in all Cargo.toml files and in the module metadata.
/// Set the version number in all Cargo.toml files.
#[derive(Debug, Parser)]
pub struct SetVersionCli {
/// Version number.