Compare commits

...

56 Commits

Author SHA1 Message Date
Andrew Gunnerson cf5ef13e47 Version 3.6.0
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-08-19 00:03:38 -04:00
Andrew Gunnerson 5fada419cb CHANGELOG.md: Add entry for PR #335
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-08-19 00:03:05 -04:00
Andrew Gunnerson eeea9f41b4 cli/args: Use tracing::Level directly
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-08-18 23:58:11 -04:00
Andrew Gunnerson 0bebf120c6 CHANGELOG.md: Add entry for PR #334
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-08-18 23:57:40 -04:00
Andrew Gunnerson 264c602fdb cli/ota: Remove unnecessary mutex
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-08-18 23:49:41 -04:00
Andrew Gunnerson 343e2e279c CHANGELOG.md: Add entry for PR #333
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-08-18 23:44:45 -04:00
Andrew Gunnerson 59ca759262 Add support for gzip VABC algorithm
Older devices, like the Pixel 4a 5G (bramble) use gzip instead of lz4.

This commit also reworks the CoW size estimate calculation to add the
same constant headroom that AOSP's delta_generator adds. Previously,
avbroot was already adding an additional 1% to account for differences
in compression ratios across compression library implementations. This
papered over the issue for large partitions, but small partitions could
still have a CoW size estimate that's too small. Adding the constant
headroom prevents ENOSPC when flashing those partitions.

Fixes: #332

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-08-18 23:33:43 -04:00
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
34 changed files with 2187 additions and 741 deletions
+35 -4
View File
@@ -19,6 +19,7 @@ jobs:
# 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:
@@ -33,6 +34,12 @@ jobs:
- 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@v4
@@ -40,6 +47,26 @@ jobs:
# 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
@@ -63,7 +90,8 @@ jobs:
shell: bash
run: |
for target in ${TARGETS}; do
cargo clippy --release --workspace --features static \
cargo android \
clippy --release --workspace --features static \
--target "${target}"
done
@@ -71,7 +99,8 @@ jobs:
shell: bash
run: |
for target in ${TARGETS}; do
cargo build --release --workspace --features static \
cargo android \
build --release --workspace --features static \
--target "${target}"
done
@@ -79,7 +108,8 @@ jobs:
shell: bash
run: |
for target in ${TARGETS}; do
cargo test --release --workspace --features static \
cargo android \
test --release --workspace --features static \
--target "${target}"
done
@@ -87,7 +117,8 @@ jobs:
shell: bash
run: |
for target in ${TARGETS}; do
cargo run --release -p e2e --features static \
cargo android \
run --release -p e2e --features static \
--target "${target}" \
-- test -a -c e2e/e2e.toml
done
+65
View File
@@ -7,6 +7,48 @@
to update the actual links at the bottom of the file.
-->
### Version 3.6.0
* Add support for gzip compression when computing CoW size estimates ([Issue #332], [PR #333])
* This allows `--replace` to successfully replace dynamic partitions on legacy devices, like the Pixel 4a 5G
* Minor code cleanup ([PR #334], [PR #335])
### 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])
@@ -177,6 +219,7 @@ Behind-the-scenes changes:
[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
@@ -194,6 +237,12 @@ Behind-the-scenes changes:
[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
[Issue #332]: https://github.com/chenxiaolong/avbroot/issues/332
[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
@@ -272,9 +321,25 @@ Behind-the-scenes changes:
[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
[PR #333]: https://github.com/chenxiaolong/avbroot/pull/333
[PR #334]: https://github.com/chenxiaolong/avbroot/pull/334
[PR #335]: https://github.com/chenxiaolong/avbroot/pull/335
Generated
+263 -244
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 = "3.2.0"
version = "3.6.0"
license = "GPL-3.0-only"
edition = "2021"
repository = "https://github.com/chenxiaolong/avbroot"
+44
View File
@@ -32,6 +32,10 @@ This subcommand packs a new AVB image from the `avb.toml` file and, for appended
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
@@ -248,3 +252,43 @@ 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).
+55 -29
View File
@@ -15,9 +15,9 @@ Having a good understanding of how AVB and A/B OTAs work is recommended prior to
* `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. All Pixel devices with unlockable bootloaders since the Pixel 2 support this. Other devices may support it as well, but there's no easy way to check without just trying it.
* 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.
* NOTE: Some OnePlus devices have a broken implementation where a custom public key can be set, but the device won't boot despite having proper signatures. Downgrading the bootloader to the version shipped with Android 11 might potentially help. This problem has been reported across multiple OnePlus models ([#186](https://github.com/chenxiaolong/avbroot/issues/186), [#195](https://github.com/chenxiaolong/avbroot/discussions/195), [#212](https://github.com/chenxiaolong/avbroot/issues/212)).
A list of devices known to work can be found in the issue tracker at [#299](https://github.com/chenxiaolong/avbroot/issues/299).
## Patches
@@ -35,7 +35,11 @@ avbroot applies the following patches to the partition images:
Repeat: **_ALWAYS leave `OEM unlocking` enabled if rooted._**
* 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). This includes 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 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:
* The `Direct install` method for updating Magisk. Magisk updates **must** be done by repatching the OTA, not via the app.
* 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.
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.
@@ -126,21 +130,27 @@ When patching OTAs for multiple devices, generating unique keys for each device
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 PEM-encoded X509 certificate, 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 (triggering a data wipe). Follow the [Usage section](#usage) as if doing an initial setup.
## Initial setup
1. Reboot into fastboot mode and unlock the bootloader if it isn't already unlocked. This will trigger a data wipe.
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
fastboot --version
```
2. Reboot into fastboot mode and unlock the bootloader if it isn't already unlocked. This will trigger a data wipe.
```bash
fastboot flashing unlock
```
2. When setting things up for the first time, the device must already be running the correct OS. Flash the original unpatched OTA if needed.
3. When setting things up for the first time, the device must already be running the correct OS. Flash the original unpatched OTA if needed.
3. Extract the partition images from the patched OTA that are different from the original.
4. Extract the partition images from the patched OTA that are different from the original.
```bash
avbroot ota extract \
@@ -151,7 +161,7 @@ If you lose your AVB or OTA signing key, you will no longer be able to sign new
If you prefer to extract and flash all OS partitions just to be safe, pass in `--all`.
4. Flash the partition images that were extracted.
5. Flash the partition images that were extracted.
```bash
ANDROID_PRODUCT_OUT=extracted fastboot flashall --skip-reboot
@@ -161,7 +171,7 @@ If you lose your AVB or OTA signing key, you will no longer be able to sign new
Alternatively, for Pixel devices, running `flash-base.sh` from the factory image will also update the bootloader and modem.
5. Set up the custom AVB public key in the bootloader after rebooting from fastbootd to bootloader.
6. Set up the custom AVB public key in the bootloader after rebooting from fastbootd to bootloader.
```bash
fastboot reboot-bootloader
@@ -169,7 +179,7 @@ If you lose your AVB or OTA signing key, you will no longer be able to sign new
fastboot flash avb_custom_key /path/to/avb_pkmd.bin
```
6. **[Optional]** Before locking the bootloader, reboot into Android once to confirm that everything is properly signed.
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:
@@ -183,7 +193,7 @@ If you lose your AVB or OTA signing key, you will no longer be able to sign new
init: [libfs_avb]Returning avb_handle with status: Success
```
7. Reboot back into fastboot and lock the bootloader. This will trigger a data wipe again.
8. Reboot back into fastboot and lock the bootloader. This will trigger a data wipe again.
```bash
fastboot flashing lock
@@ -195,7 +205,7 @@ If you lose your AVB or OTA signing key, you will no longer be able to sign new
**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.
8. That's it! To install future OS, Magisk, or KernelSU updates, see the [next section](#updates).
9. That's it! To install future OS, Magisk, or KernelSU updates, see the [next section](#updates).
## Updates
@@ -426,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:
@@ -440,25 +472,19 @@ Debug builds work too, but they will run significantly slower (in the sha256 com
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. This is the same key listed in [the author's profile](https://github.com/chenxiaolong/).
```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
+21 -11
View File
@@ -13,9 +13,9 @@ avbroot – это программа для модификации OTA-обра
* наличие файла `payload.bin` (обычно находится в корне архива)
* наличие файла `META-INF/com/android/metadata` (Android 10-11) или `META-INF/com/android/metadata.pb` (Android 12+)
* Устройство должно поддерживать установку пользовательского публичного ключа для подтверждения статуса доверия загрузчика. Обычно это делается с помощью команды `fastboot flash avb_custom_key`. Все устройства Pixel с разблокируемым загрузчиком, начиная с Pixel 2, поддерживают эту функцию. Другие устройства тоже могут поддерживать её, но достоверно убедиться в этом можно только проверив лично.
* Устройство должно поддерживать установку пользовательского публичного ключа для подтверждения статуса доверия загрузчика. Обычно это производится с помощью команды `fastboot flash avb_custom_key`.
* ПРИМЕЧАНИЕ: Некоторые девайсы от OnePlus имеют некорректную реализацию, где возможна установка кастомного публичного ключа, но устройство все равно не будет загружаться, несмотря на наличие корректной подписи. Понижение загрузчика до версии, поставляемой вместе с Android 11, потенциально может помочь. Уже сообщалось о наличии этой проблемы на нескольких устройствах OnePlus ([#186,](https://github.com/chenxiaolong/avbroot/issues/186) [#195,](https://github.com/chenxiaolong/avbroot/discussions/195) [#212](https://github.com/chenxiaolong/avbroot/issues/212)).
Список девайсов, на которых проверялась совместимость с указанным выше функционалом, находится здесь: [#299.](https://github.com/chenxiaolong/avbroot/issues/299)
## Патчи
@@ -33,7 +33,11 @@ avbroot модифицирует следующие образы:
Повторюсь: **_ВСЕГДА оставляйте `Заводскую разблокировку` включенной при наличии root-прав._**
* Любая операция, приводящая к прошивке некорректно подписанного загрузочного образа, приведет к тому, что устройство больше не сможет загрузиться в систему/режим Recovery, а для его восстановления потребуется повторная разблокировка загрузчика (и, следовательно, стирание всех пользовательских данных). Это же относится и к методу `Прямой установки` для обновления Magisk. Обновление Magisk **должно выполняться только путем обновления OTA,** а не через Magisk Manager.
* Любая операция, приводящая к прошивке некорректно подписанного загрузочного образа, приведет к тому, что устройство больше не сможет загрузиться в систему/режим Recovery, а для его восстановления потребуется повторная разблокировка загрузчика (и, следовательно, стирание всех пользовательских данных). К подобным операциям в том числе относятся:
* Метод `Прямой установки` для обновления Magisk. Magisk можно обновлять **только путем репатчинга OTA,** но не через его приложение.
* Функция `Удаление Magisk` в приложении Magisk. Если вам больше не нужен root-доступ, Magisk **должен быть удален путем репатчинга OTA** с использованием параметра `--rootless`, но не через его приложение.
Если в загрузочный раздел были внесены какие-либо изменения, **не перезагружайтесь**. Обратитесь за помощью, [открыв Issue,](https://github.com/chenxiaolong/avbroot/issues/new) и четко разъясните, какие конкретные действия привели к возникновению такой ситуации. Если Android всё еще работает и доступ к root-правам сохранился – вероятно, получится откатить изменения до исходного состояния, не стирая ваши данные.
@@ -130,15 +134,21 @@ avbroot совместим с любым стандартным 4096-битны
## Первоначальная настройка
1. Перезагрузитесь в режим fastboot и разблокируйте загрузчик, если не сделали этого ранее. Это приведет к стиранию всех пользовательских данных.
1. Убедитесь, что вы используете утилиту fastboot версии 34 или новее. Предыдущие версии содержат баги, что не позволяют команде `fastboot flashall` (которая понадобится по ходу инструкции) работать правильно.
```bash
fastboot --version
```
2. Перезагрузитесь в режим fastboot и разблокируйте загрузчик, если не сделали этого ранее. Это приведет к стиранию всех пользовательских данных.
```bash
fastboot flashing unlock
```
2. Перед первой установкой, на устройстве уже должна быть установлена в оригинальном виде та прошивка, пропатченную версию которой вы собираетесь ставить. Если это не так, сначала установите оригинальную непропатченную OTA.
3. Перед первой установкой, на устройстве уже должна быть установлена в оригинальном виде та прошивка, пропатченную версию которой вы собираетесь ставить. Если это не так, сначала установите оригинальную непропатченную OTA.
3. Извлекаем из пропатченного OTA модифицированные образы:
4. Извлекаем из пропатченного OTA модифицированные образы:
```bash
avbroot ota extract \
@@ -149,7 +159,7 @@ avbroot совместим с любым стандартным 4096-битны
Если вы на всякий случай хотите прошить вообще все разделы из ОТА, извлечь их можно, указав аргумент `--all`.
4. Прошейте извлеченные образы разделов.
5. Прошейте извлеченные образы разделов.
```bash
ANDROID_PRODUCT_OUT=extracted fastboot flashall --skip-reboot
@@ -159,7 +169,7 @@ avbroot совместим с любым стандартным 4096-битны
Для устройств Pixel есть ещё один вариант: запуск скрипта `flash-base.sh` из папки заводских образов (factory images) обновит загрузчик и модем.
5. После перезагрузки из fastbootd в загрузчик (bootloader), установите пользовательский публичный ключ AVB в загрузчик:
6. После перезагрузки из fastbootd в загрузчик (bootloader), установите пользовательский публичный ключ AVB в загрузчик:
```bash
fastboot reboot-bootloader
@@ -167,7 +177,7 @@ avbroot совместим с любым стандартным 4096-битны
fastboot flash avb_custom_key /путь/к/avb_pkmd.bin
```
6. **[Опционально]** Перед блокировкой загрузчика загрузитесь в систему, дабы убедиться, что все подписано правильно.
7. **[Опционально]** Перед блокировкой загрузчика загрузитесь в систему, дабы убедиться, что все подписано правильно.
Установите приложение Magisk или KernelSU и выполните следующую команду:
@@ -181,7 +191,7 @@ avbroot совместим с любым стандартным 4096-битны
init: [libfs_avb]Returning avb_handle with status: Success
```
7. Перезагрузитесь в fastboot и заблокируйте загрузчик. Это снова приведет к стиранию данных.
8. Перезагрузитесь в fastboot и заблокируйте загрузчик. Это снова приведет к стиранию данных.
```bash
fastboot flashing lock
@@ -193,7 +203,7 @@ avbroot совместим с любым стандартным 4096-битны
**ПРЕДУПРЕЖДЕНИЕ**: Если вы прошили CalyxOS, мастер настройки [автоматически отключит опцию `Заводской разблокировки`.](https://github.com/CalyxOS/platform_packages_apps_SetupWizard/blob/7d2df25cedcbff83ddb608e628f9d97b38259c26/src/org/lineageos/setupwizard/SetupWizardApp.java#L135-L140) Не забудьте снова включить её вручную в настройках для разработчиков. Для перестраховки можете использовать [модуль `OEMUnlockOnBoot`,](https://github.com/chenxiaolong/OEMUnlockOnBoot) который автоматически включает пункт Заводской разблокировки при каждом запуске системы.
8. Готово. Установка последующих обновлений системы, Magisk или KernelSU, описывается в [следующем разделе.](#обновления)
9. Готово! Установка последующих обновлений системы, Magisk или KernelSU, описывается в [следующем разделе.](#обновления)
## Обновления
+5 -3
View File
@@ -27,11 +27,12 @@ hex = { version = "0.4.3", features = ["serde"] }
liblzma = "0.3.0"
lz4_flex = "0.11.1"
memchr = "2.6.0"
miniz_oxide = "0.8.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"] }
@@ -71,8 +72,9 @@ features = ["deflate"]
rustix = { version = "0.38.9", default-features = false, features = ["process"] }
[build-dependencies]
prost-build = "0.12.1"
protox = "0.6.0"
constcat = "0.5.0"
prost-build = "0.13.1"
protox = "0.7.0"
[dev-dependencies]
assert_matches = "1.5.0"
+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();
}
+8 -39
View File
@@ -1,5 +1,5 @@
/*
* SPDX-FileCopyrightText: 2023 Andrew Gunnerson
* SPDX-FileCopyrightText: 2023-2024 Andrew Gunnerson
* SPDX-License-Identifier: GPL-3.0-only
*/
@@ -15,7 +15,7 @@ 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, hashtree, key, ota};
use crate::cli::{avb, boot, completion, cpio, fec, hashtree, key, ota, payload};
#[allow(clippy::large_enum_variant)]
#[derive(Debug, Subcommand)]
@@ -28,6 +28,7 @@ pub enum Command {
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.)
@@ -36,39 +37,6 @@ 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,
@@ -95,8 +63,8 @@ pub struct Cli {
pub command: Command,
/// Lowest log message severity to output.
#[arg(long, global = true, value_name = "LEVEL", default_value_t)]
pub log_level: LogLevel,
#[arg(long, global = true, value_name = "LEVEL", default_value_t = Level::INFO)]
pub log_level: Level,
/// Output format for log messages.
#[arg(long, global = true, value_name = "FORMAT", default_value_t)]
@@ -123,11 +91,11 @@ impl FormatTime for ShortUptime {
}
}
pub fn init_logging(log_level: LogLevel, log_format: LogFormat) {
pub fn init_logging(log_level: Level, 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());
.with_max_level(log_level);
match log_format {
LogFormat::Short => {
@@ -163,6 +131,7 @@ pub fn main(logging_initialized: &AtomicBool, cancel_signal: &AtomicBool) -> Res
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),
+68 -16
View File
@@ -25,7 +25,7 @@ use serde::{Deserialize, Serialize};
use tracing::{debug_span, info, warn, Span};
use crate::{
crypto::{self, PassphraseSource},
crypto::{self, PassphraseSource, RsaSigningKey},
format::avb::{
self, AlgorithmType, AppendedDescriptorMut, AppendedDescriptorRef, Descriptor, Footer,
HashTreeDescriptor, Header, KernelCmdlineDescriptor,
@@ -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")?;
@@ -377,12 +383,26 @@ 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 => {
@@ -625,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(())
}
@@ -659,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.
@@ -733,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>,
@@ -758,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.
@@ -827,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,
+7
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:?}"))?;
@@ -93,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>,
+2 -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
*/
@@ -12,3 +12,4 @@ pub mod fec;
pub mod hashtree;
pub mod key;
pub mod ota;
pub mod payload;
+107 -36
View File
@@ -21,7 +21,6 @@ use cap_std::{ambient_authority, fs::Dir};
use cap_tempfile::TempDir;
use clap::{value_parser, ArgAction, Args, Parser, Subcommand};
use rayon::{iter::IntoParallelRefIterator, prelude::ParallelIterator};
use rsa::RsaPrivateKey;
use tempfile::NamedTempFile;
use topological_sort::TopologicalSort;
use tracing::{debug_span, info, warn};
@@ -30,12 +29,12 @@ use zip::{write::FileOptions, CompressionMethod, ZipArchive, ZipWriter};
use crate::{
cli,
crypto::{self, PassphraseSource},
crypto::{self, PassphraseSource, RsaSigningKey},
format::{
avb::{self, Descriptor, Header},
ota::{self, SigningWriter, ZipEntry},
padding,
payload::{self, PayloadHeader, PayloadWriter},
payload::{self, PayloadHeader, PayloadWriter, VabcAlgo},
},
patch::{
boot::{
@@ -197,7 +196,7 @@ fn patch_boot_images<'a, 'b: 'a>(
required_images: &'b RequiredImages,
input_files: &mut HashMap<String, InputFile>,
boot_patchers: Vec<Box<dyn BootImagePatch + Sync>>,
key_avb: &RsaPrivateKey,
key_avb: &RsaSigningKey,
cancel_signal: &AtomicBool,
) -> Result<()> {
let input_files = Mutex::new(input_files);
@@ -241,7 +240,7 @@ fn patch_system_image<'a, 'b: 'a>(
required_images: &'b RequiredImages,
input_files: &mut HashMap<String, InputFile>,
cert_ota: &Certificate,
key_avb: &RsaPrivateKey,
key_avb: &RsaSigningKey,
cancel_signal: &AtomicBool,
) -> Result<(&'b str, Vec<Range<u64>>)> {
let Some(target) = required_images.iter_system().next() else {
@@ -565,7 +564,7 @@ fn update_vbmeta_headers(
headers: &mut HashMap<String, Header>,
order: &mut [(String, HashSet<String>)],
clear_vbmeta_flags: bool,
key: &RsaPrivateKey,
key: &RsaSigningKey,
block_size: u64,
) -> Result<()> {
for (name, deps) in order {
@@ -624,7 +623,7 @@ fn update_vbmeta_headers(
/// If `ranges` is [`None`], then the entire file is compressed. Otherwise, only
/// the chunks containing the specified ranges are compressed. In the latter
/// scenario, unmodified chunks must be copied from the original payload.
fn compress_image(
pub fn compress_image(
name: &str,
file: &mut PSeekFile,
header: &mut PayloadHeader,
@@ -674,12 +673,43 @@ fn compress_image(
info!("Compressing full image: {name}");
// Otherwise, compress the entire image.
let (partition_info, operations) =
payload::compress_image(&*file, &writer, name, block_size, cancel_signal)?;
// Otherwise, compress the entire image. If VABC is enabled, we need to
// update the CoW size estimate or else the CoW block device may run out of
// space during flashing.
let vabc_algo = if partition.estimate_cow_size.is_some() {
info!("Needs updated CoW size estimate: {name}");
// Only CoW v2 seems to exist in the wild currently, so that is all we
// support.
let Some(dpm) = &header.manifest.dynamic_partition_metadata else {
bail!("Dynamic partition metadata is missing");
};
if !dpm.vabc_enabled() {
bail!("Partition has CoW estimate, but VABC is disabled: {name}");
}
let cow_version = dpm.cow_version();
if dpm.cow_version() != 2 {
bail!("Unsupported CoW version: {cow_version}");
}
let compression = dpm.vabc_compression_param();
let Some(vabc_algo) = VabcAlgo::new(compression) else {
bail!("Unsupported VABC compression: {compression}");
};
Some(vabc_algo)
} else {
None
};
let (partition_info, operations, cow_estimate) =
payload::compress_image(&*file, &writer, name, block_size, vabc_algo, cancel_signal)?;
partition.new_partition_info = Some(partition_info);
partition.operations = operations;
partition.estimate_cow_size = cow_estimate;
*file = writer;
@@ -694,20 +724,18 @@ fn patch_ota_payload(
external_images: &HashMap<String, PathBuf>,
boot_patchers: Vec<Box<dyn BootImagePatch + Sync>>,
clear_vbmeta_flags: bool,
key_avb: &RsaPrivateKey,
key_ota: &RsaPrivateKey,
key_avb: &RsaSigningKey,
key_ota: &RsaSigningKey,
cert_ota: &Certificate,
cancel_signal: &AtomicBool,
) -> Result<(String, u64)> {
let header = PayloadHeader::from_reader(payload.reopen_boxed()?)
let mut header = PayloadHeader::from_reader(payload.reopen_boxed()?)
.context("Failed to load OTA payload header")?;
if !header.is_full_ota() {
bail!("Payload is a delta OTA, not a full OTA");
}
let header = Mutex::new(header);
let mut header_locked = header.lock().unwrap();
let all_partitions = header_locked
let all_partitions = header
.manifest
.partitions
.iter()
@@ -725,7 +753,7 @@ fn patch_ota_payload(
// Determine what images need to be patched. For simplicity, we pre-read all
// vbmeta images since they're tiny. They're discarded later if the they
// don't need to be modified.
let required_images = RequiredImages::new(&header_locked.manifest);
let required_images = RequiredImages::new(&header.manifest);
let vbmeta_images = required_images.iter_vbmeta().collect::<HashSet<_>>();
// The set of source images to be inserted into the new payload, replacing
@@ -737,7 +765,7 @@ fn patch_ota_payload(
payload,
&required_images,
external_images,
&header_locked,
&header,
cancel_signal,
)?;
@@ -779,7 +807,7 @@ fn patch_ota_payload(
&mut vbmeta_order,
clear_vbmeta_flags,
key_avb,
header_locked.manifest.block_size().into(),
header.manifest.block_size().into(),
)?;
// Unmodified vbmeta images no longer need to be kept around either.
@@ -791,7 +819,7 @@ fn patch_ota_payload(
let modified_operations = compress_image(
&name,
&mut input_file.file,
&mut header_locked,
&mut header,
// We can only perform the optimization of avoiding
// recompression if the image came from the original payload.
if name == system_target && !external_images.contains_key(&name) {
@@ -809,7 +837,7 @@ fn patch_ota_payload(
info!("Generating new OTA payload");
let mut payload_writer = PayloadWriter::new(writer, header_locked.clone(), key_ota.clone())
let mut payload_writer = PayloadWriter::new(writer, header.clone(), key_ota.clone())
.context("Failed to write payload header")?;
let mut orig_payload_reader = payload.reopen_boxed().context("Failed to open payload")?;
@@ -827,7 +855,7 @@ fn patch_ota_payload(
let pi = payload_writer.partition_index().unwrap();
let oi = payload_writer.operation_index().unwrap();
let orig_partition = &header_locked.manifest.partitions[pi];
let orig_partition = &header.manifest.partitions[pi];
let orig_operation = &orig_partition.operations[oi];
let data_offset = orig_operation
.data_offset
@@ -857,7 +885,7 @@ fn patch_ota_payload(
// Otherwise, copy from the original payload.
let data_offset = data_offset
.checked_add(header_locked.blob_offset)
.checked_add(header.blob_offset)
.ok_or_else(|| anyhow!("data_offset overflow in partition #{pi} operation #{oi}"))?;
orig_payload_reader
@@ -873,7 +901,7 @@ fn patch_ota_payload(
.with_context(|| format!("Failed to copy from original payload: {name}"))?;
}
let (_, properties, metadata_size) = payload_writer
let (_, _, properties, metadata_size) = payload_writer
.finish()
.context("Failed to finalize payload")?;
@@ -888,8 +916,8 @@ fn patch_ota_zip(
external_images: &HashMap<String, PathBuf>,
mut boot_patchers: Vec<Box<dyn BootImagePatch + Sync>>,
clear_vbmeta_flags: bool,
key_avb: &RsaPrivateKey,
key_ota: &RsaPrivateKey,
key_avb: &RsaSigningKey,
key_ota: &RsaSigningKey,
cert_ota: &Certificate,
cancel_signal: &AtomicBool,
) -> Result<(OtaMetadata, u64)> {
@@ -1064,7 +1092,7 @@ fn patch_ota_zip(
Ok((metadata, payload_metadata_size.unwrap()))
}
fn extract_ota_zip(
pub fn extract_payload(
raw_reader: &PSeekFile,
directory: &Dir,
payload_offset: u64,
@@ -1190,10 +1218,38 @@ pub fn patch_subcommand(cli: &PatchCli, cancel_signal: &AtomicBool) -> Result<()
cli.pass_ota_env_var.as_deref(),
);
let key_avb = crypto::read_pem_key_file(&cli.key_avb, &source_avb)
.with_context(|| format!("Failed to load key: {:?}", cli.key_avb))?;
let key_ota = crypto::read_pem_key_file(&cli.key_ota, &source_ota)
.with_context(|| format!("Failed to load key: {:?}", cli.key_ota))?;
let (key_avb, key_ota) = if let Some(helper) = &cli.signing_helper {
let public_key_avb = crypto::read_pem_public_key_file(&cli.key_avb)
.with_context(|| format!("Failed to load key: {:?}", cli.key_avb))?;
let public_key_ota = crypto::read_pem_public_key_file(&cli.key_ota)
.with_context(|| format!("Failed to load key: {:?}", cli.key_ota))?;
let key_avb = RsaSigningKey::External {
program: helper.clone(),
public_key_file: cli.key_avb.clone(),
public_key: public_key_avb,
passphrase_source: source_avb,
};
let key_ota = RsaSigningKey::External {
program: helper.clone(),
public_key_file: cli.key_ota.clone(),
public_key: public_key_ota,
passphrase_source: source_ota,
};
(key_avb, key_ota)
} else {
let private_key_avb = crypto::read_pem_key_file(&cli.key_avb, &source_avb)
.with_context(|| format!("Failed to load key: {:?}", cli.key_avb))?;
let private_key_ota = crypto::read_pem_key_file(&cli.key_ota, &source_ota)
.with_context(|| format!("Failed to load key: {:?}", cli.key_ota))?;
let key_avb = RsaSigningKey::Internal(private_key_avb);
let key_ota = RsaSigningKey::Internal(private_key_ota);
(key_avb, key_ota)
};
let cert_ota = crypto::read_pem_cert_file(&cli.cert_ota)
.with_context(|| format!("Failed to load certificate: {:?}", cli.cert_ota))?;
@@ -1387,7 +1443,7 @@ pub fn extract_subcommand(cli: &ExtractCli, cancel_signal: &AtomicBool) -> Resul
let directory = Dir::open_ambient_dir(&cli.directory, authority)
.with_context(|| format!("Failed to open directory: {:?}", cli.directory))?;
extract_ota_zip(
extract_payload(
&raw_reader,
&directory,
payload_offset,
@@ -1599,7 +1655,7 @@ pub fn verify_subcommand(cli: &VerifyCli, cancel_signal: &AtomicBool) -> Result<
.cloned()
.collect::<BTreeSet<_>>();
extract_ota_zip(
extract_payload(
&raw_reader,
&temp_dir,
pf_payload.offset,
@@ -1720,7 +1776,10 @@ pub struct PatchCli {
#[arg(short, long, value_name = "FILE", value_parser, help_heading = HEADING_PATH)]
pub output: Option<PathBuf>,
/// Private key for signing vbmeta images.
/// Signing key for vbmeta headers.
///
/// This should normally be a private key. However, if --signing-helper is
/// used, then it should be a public key instead.
#[arg(
long,
alias = "privkey-avb",
@@ -1730,7 +1789,10 @@ pub struct PatchCli {
)]
pub key_avb: PathBuf,
/// Private key for signing the OTA.
/// Signing key for the OTA.
///
/// This should normally be a private key. However, if --signing-helper is
/// used, then it should be a public key instead.
#[arg(
long,
alias = "privkey-ota",
@@ -1788,13 +1850,22 @@ pub struct PatchCli {
)]
pub pass_ota_file: Option<PathBuf>,
/// External program for signing.
///
/// If this option is specified, then --key-avb and --key-ota must refer to
/// public keys. The program will be invoked as:
///
/// <program> <algo> <public key> [file <pass file>|env <pass env>]
#[arg(long, value_name = "PROGRAM", value_parser, help_heading = HEADING_KEY)]
pub signing_helper: Option<PathBuf>,
/// Use partition image from a file instead of the original payload.
#[arg(
long,
value_names = ["PARTITION", "FILE"],
value_parser = value_parser!(OsString),
num_args = 2,
help_heading = HEADING_PATH,
help_heading = HEADING_PATH
)]
pub replace: Vec<OsString>,
+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,
}
+252 -9
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();
@@ -228,6 +454,16 @@ 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)?;
@@ -237,6 +473,14 @@ pub fn write_pem_public_key(mut writer: impl Write, key: &RsaPublicKey) -> Resul
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)?;
@@ -351,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)
@@ -389,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,
+168 -176
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::{
cmp, fmt,
fmt,
io::{self, Cursor, Read, Seek, SeekFrom, Write},
ops::Range,
str,
@@ -16,12 +16,12 @@ use byteorder::{BigEndian, ReadBytesExt, WriteBytesExt};
use num_bigint_dig::{ModInverse, ToBigInt};
use num_traits::{Pow, ToPrimitive};
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},
@@ -29,8 +29,8 @@ use crate::{
padding,
},
stream::{
self, CountingReader, FromReader, ReadDiscardExt, ReadSeekReopen, ReadStringExt, ToWriter,
WriteSeekReopen, WriteStringExt, WriteZerosExt,
self, CountingReader, CountingWriter, FromReader, ReadDiscardExt, ReadSeekReopen,
ReadStringExt, ToWriter, WriteSeekReopen, WriteStringExt, WriteZerosExt,
},
util,
};
@@ -50,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 {
@@ -86,12 +103,9 @@ 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("Hash tree does not immediately follow image data")]
HashTreeGap,
@@ -103,16 +117,18 @@ 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),
}
@@ -137,6 +153,7 @@ pub enum AlgorithmType {
Sha512Rsa2048,
Sha512Rsa4096,
Sha512Rsa8192,
#[serde(untagged)]
Unknown(u32),
}
@@ -167,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,
@@ -198,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())
}
}
@@ -1442,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] {
@@ -1462,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);
@@ -1506,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();
@@ -1806,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)
}
@@ -1834,97 +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 = if footer.is_some() {
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("eof_image_size"))?,
AppendedDescriptorRef::Hash(d) => d.image_size,
}
} else {
0
};
writer.seek(SeekFrom::Start(eof_image_size))?;
// 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;
+4 -4
View File
@@ -166,8 +166,8 @@ impl Fec {
return Err(Error::UnsupportedParity(parity));
}
let blocks = util::div_ceil(file_size, u64::from(block_size));
let rounds = util::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
@@ -196,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
}
@@ -225,7 +225,7 @@ impl Fec {
let end_block = if range.end % block_size == 0 {
range.end / block_size
} else {
util::div_ceil(range.end, block_size)
range.end.div_ceil(block_size)
};
for block in start_block..end_block {
+3 -3
View File
@@ -72,14 +72,14 @@ impl HashTree {
/// 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.
fn compute_level_offsets(&self, image_size: u64) -> Result<Vec<Range<usize>>> {
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 = util::div_ceil(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)))
@@ -124,7 +124,7 @@ impl HashTree {
let end_block = if range.end % block_size == 0 {
range.end / block_size
} else {
util::div_ceil(range.end, block_size)
range.end.div_ceil(block_size)
};
result.push(start_block..end_block);
+6 -11
View File
@@ -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},
@@ -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")]
@@ -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(),
+110 -28
View File
@@ -1,10 +1,11 @@
/*
* SPDX-FileCopyrightText: 2022-2023 Andrew Gunnerson
* SPDX-FileCopyrightText: 2022-2024 Andrew Gunnerson
* SPDX-License-Identifier: GPL-3.0-only
*/
use std::{
collections::{HashMap, HashSet},
fmt,
io::{self, Cursor, Read, Seek, SeekFrom, Write},
ops::Range,
sync::atomic::AtomicBool,
@@ -26,13 +27,12 @@ use rayon::{
prelude::{IntoParallelIterator, IntoParallelRefIterator, ParallelIterator},
};
use ring::digest::{Context, Digest};
use rsa::{traits::PublicKeyParts, Pkcs1v15Sign, RsaPrivateKey};
use sha2::Sha256;
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,
@@ -100,19 +100,19 @@ pub enum Error {
ProtobufDecode(#[from] prost::DecodeError),
#[error("XZ stream error")]
XzStream(#[from] liblzma::stream::Error),
#[error("RSA error")]
Rsa(#[from] rsa::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,
}
@@ -175,9 +175,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",
@@ -214,8 +213,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),
}
@@ -292,7 +290,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.
@@ -315,7 +313,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.
@@ -390,12 +388,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)?;
@@ -419,7 +417,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
@@ -890,6 +888,52 @@ fn compress_chunk(raw_data: &[u8], cancel_signal: &AtomicBool) -> Result<(Vec<u8
Ok((data, digest_compressed))
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Deserialize, Serialize)]
pub enum VabcAlgo {
Lz4,
Gzip,
}
impl VabcAlgo {
pub fn new(name: &str) -> Option<Self> {
match name {
"lz4" => Some(Self::Lz4),
"gz" => Some(Self::Gzip),
_ => None,
}
}
fn compressed_size(&self, 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 = match self {
Self::Lz4 => lz4_flex::block::compress(&raw_data[..n]),
// We use the miniz_oxide backend for flate2, but flate2 doesn't
// expose a nice function for compressing to a vec, so just use
// miniz_oxide directly.
Self::Gzip => miniz_oxide::deflate::compress_to_vec_zlib(&raw_data[..n], 9),
};
total += compressed.len().min(n) as u64;
raw_data = &raw_data[n..];
}
total
}
}
impl fmt::Display for VabcAlgo {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Lz4 => f.write_str("lz4"),
Self::Gzip => f.write_str("gz"),
}
}
}
/// 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
@@ -897,13 +941,21 @@ fn compress_chunk(raw_data: &[u8], cancel_signal: &AtomicBool) -> Result<(Vec<u8
/// 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 `vabc_algo` is set, the VABC CoW v2 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,
vabc_algo: Option<VabcAlgo>,
cancel_signal: &AtomicBool,
) -> Result<(PartitionInfo, Vec<InstallOperation>)> {
) -> Result<(PartitionInfo, Vec<InstallOperation>, Option<u64>)> {
const CHUNK_SIZE: u64 = 2 * 1024 * 1024;
const CHUNK_GROUP: u64 = 32;
@@ -918,9 +970,10 @@ pub fn compress_image(
});
}
let chunks_total = util::div_ceil(file_size, CHUNK_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
@@ -957,8 +1010,13 @@ pub fn compress_image(
let mut compressed_data_group = uncompressed_data_group
.into_par_iter()
.map(
|(raw_offset, raw_data)| -> Result<(Vec<u8>, InstallOperation)> {
|(raw_offset, raw_data)| -> Result<(Vec<u8>, InstallOperation, u64)> {
let (data, digest_compressed) = compress_chunk(&raw_data, cancel_signal)?;
let cow_size = if let Some(algo) = vabc_algo {
algo.compressed_size(&raw_data, block_size)
} else {
0
};
let extent = Extent {
start_block: Some(raw_offset / u64::from(block_size)),
@@ -971,19 +1029,20 @@ pub fn compress_image(
operation.dst_extents.push(extent);
operation.data_sha256_hash = Some(digest_compressed.as_ref().to_vec());
Ok((data, operation))
Ok((data, operation, cow_size))
},
)
.collect::<Result<Vec<_>>>()?;
for (data, operation) in &mut compressed_data_group {
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> {
.map(|(data, operation, _)| -> Result<InstallOperation> {
let mut writer = output.reopen_boxed()?;
writer.seek(SeekFrom::Start(operation.data_offset.unwrap()))?;
writer.write_all(&data)?;
@@ -1001,7 +1060,30 @@ pub fn compress_image(
hash: Some(digest_uncompressed.as_ref().to_vec()),
};
Ok((partition_info, operations))
let cow_estimate = if vabc_algo.is_some() {
// lz4_flex and miniz_oxide usually compress better than the lz4 and
// zlib implementations used by libsnapshot_cow. Make up for this by
// adding percentage-based overhead.
cow_estimate += cow_estimate / 100;
// We also need to account for constant overhead, especially with
// smaller partitions. We can match what delta_generator normally adds
// in CowWriterV2::InitPos() exactly. Since we only ever create full
// OTAs, we can assume that all CoW operations are kCowReplaceOp.
// sizeof(CowHeader).
cow_estimate += 38;
// header_.buffer_size (equal to BUFFER_REGION_DEFAULT_SIZE).
cow_estimate += 2 * 1024 * 1024;
// CowOptions::cluster_ops * sizeof(CowOperationV2).
cow_estimate += 200 * 20;
Some(cow_estimate)
} else {
None
};
Ok((partition_info, operations, cow_estimate))
}
fn extents_sorted(operations: &[InstallOperation]) -> bool {
@@ -1059,7 +1141,7 @@ pub fn compress_modified_image(
return Err(Error::ExtentsNotInOrder);
}
let groups_total = util::div_ceil(operations.len(), OPERATION_GROUP);
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![];
+60 -25
View File
@@ -23,14 +23,14 @@ use liblzma::{
use rayon::iter::{IntoParallelRefIterator, IntoParallelRefMutIterator, ParallelIterator};
use regex::bytes::Regex;
use ring::digest::Context;
use rsa::{RsaPrivateKey, RsaPublicKey};
use rsa::RsaPublicKey;
use thiserror::Error;
use tracing::{debug, debug_span, trace, warn, Span};
use x509_cert::Certificate;
use zip::{result::ZipError, ZipArchive};
use crate::{
crypto,
crypto::{self, RsaSigningKey},
format::{
avb::{self, AppendedDescriptorMut, Footer, Header},
bootimage::{self, BootImage, BootImageExt, RamdiskMeta},
@@ -70,7 +70,11 @@ pub enum Error {
#[error("XZ stream error")]
XzStream(#[from] liblzma::stream::Error),
#[error("Zip error")]
Zip(#[from] ZipError),
Zip(#[source] ZipError),
#[error("Zip error for entry name: {0:?}")]
ZipEntryName(String, #[source] ZipError),
#[error("Zip error for entry index #{0}")]
ZipEntryIndex(usize, #[source] ZipError),
#[error("I/O error")]
Io(#[from] io::Error),
#[error("File I/O error")]
@@ -157,6 +161,14 @@ impl MagiskRootPatcher {
const VER_XZ_BACKUP: Range<u32> =
26403..Self::VERS_SUPPORTED[Self::VERS_SUPPORTED.len() - 1].end;
const ZIP_INIT_LD: &'static str = "lib/arm64-v8a/libinit-ld.so";
const ZIP_LIBMAGISK: &'static str = "lib/arm64-v8a/libmagisk.so";
const ZIP_LIBMAGISK32: &'static str = "lib/armeabi-v7a/libmagisk32.so";
const ZIP_LIBMAGISK64: &'static str = "lib/arm64-v8a/libmagisk64.so";
const ZIP_MAGISKINIT: &'static str = "lib/arm64-v8a/libmagiskinit.so";
const ZIP_STUB: &'static str = "assets/stub.apk";
const ZIP_UTIL_FUNCTIONS: &'static str = "assets/util_functions.sh";
pub fn new(
path: &Path,
preinit_device: Option<&str>,
@@ -208,8 +220,10 @@ impl MagiskRootPatcher {
fn get_version(path: &Path) -> Result<u32> {
let reader = File::open(path).map_err(|e| Error::File(path.to_owned(), e))?;
let reader = BufReader::new(reader);
let mut zip = ZipArchive::new(reader)?;
let entry = zip.by_name("assets/util_functions.sh")?;
let mut zip = ZipArchive::new(reader).map_err(Error::Zip)?;
let entry = zip
.by_name(Self::ZIP_UTIL_FUNCTIONS)
.map_err(|e| Error::ZipEntryName(Self::ZIP_UTIL_FUNCTIONS.to_owned(), e))?;
let mut entry = BufReader::new(entry);
let mut line = String::new();
@@ -384,7 +398,7 @@ impl BootImagePatch for MagiskRootPatcher {
fn patch(&self, boot_image: &mut BootImage, cancel_signal: &AtomicBool) -> Result<()> {
let zip_reader =
File::open(&self.apk_path).map_err(|e| Error::File(self.apk_path.clone(), e))?;
let mut zip = ZipArchive::new(BufReader::new(zip_reader))?;
let mut zip = ZipArchive::new(BufReader::new(zip_reader)).map_err(Error::Zip)?;
// Load the first ramdisk. If it doesn't exist, we have to generate one
// from scratch.
@@ -413,7 +427,9 @@ impl BootImagePatch for MagiskRootPatcher {
// Add magiskinit.
{
let mut zip_entry = zip.by_name("lib/arm64-v8a/libmagiskinit.so")?;
let mut zip_entry = zip
.by_name(Self::ZIP_MAGISKINIT)
.map_err(|e| Error::ZipEntryName(Self::ZIP_MAGISKINIT.to_owned(), e))?;
let mut data = vec![];
zip_entry.read_to_end(&mut data)?;
@@ -424,28 +440,40 @@ impl BootImagePatch for MagiskRootPatcher {
));
}
// Add xz-compressed magisk32 and magisk64. We currently unconditionally
// include magisk32 because the boot image itself doesn't contain
// sufficient information to determine if a device is 64-bit only.
let mut xz_files = HashMap::<&str, &[u8]>::new();
xz_files.insert(
"lib/armeabi-v7a/libmagisk32.so",
b"overlay.d/sbin/magisk32.xz",
);
xz_files.insert(
"lib/arm64-v8a/libmagisk64.so",
b"overlay.d/sbin/magisk64.xz",
);
if zip.file_names().any(|n| n == Self::ZIP_LIBMAGISK) {
// Newer Magisk versions only include a single binary for the target
// ABI in the ramdisk. fb5ee86615ed3df830e8538f8b39b1b133caea34.
debug!("Single libmagisk");
xz_files.insert(Self::ZIP_LIBMAGISK, b"overlay.d/sbin/magisk.xz");
} else {
// Older Magisk versions include the 64-bit binary and, optionally,
// the 32-bit binary if the device supports it. We unconditionally
// include the magisk32 because the boot image itself doesn't have
// sufficient information to determine if a device is 64-bit only.
debug!("Split libmagisk32/libmagisk64");
xz_files.insert(Self::ZIP_LIBMAGISK32, b"overlay.d/sbin/magisk32.xz");
xz_files.insert(Self::ZIP_LIBMAGISK64, b"overlay.d/sbin/magisk64.xz");
}
// Add stub apk, which only exists after Magisk commit
// ad0e6511e11ebec65aa9b5b916e1397342850319.
if zip.file_names().any(|n| n == "assets/stub.apk") {
if zip.file_names().any(|n| n == Self::ZIP_STUB) {
debug!("Magisk stub found");
xz_files.insert("assets/stub.apk", b"overlay.d/sbin/stub.xz");
xz_files.insert(Self::ZIP_STUB, b"overlay.d/sbin/stub.xz");
}
// Add init-ld, which only exists after Magisk commit
// 33aebb59763b6ec27209563035303700e998633d
if zip.file_names().any(|n| n == Self::ZIP_INIT_LD) {
debug!("Magisk init-ld found");
xz_files.insert(Self::ZIP_INIT_LD, b"overlay.d/sbin/init-ld.xz");
}
for (source, target) in xz_files {
let reader = zip.by_name(source)?;
let reader = zip
.by_name(source)
.map_err(|e| Error::ZipEntryName(source.to_owned(), e))?;
let buf = Self::xz_compress(reader, cancel_signal)?;
entries.push(CpioEntry::new_file(target, 0o644, CpioEntryData::Data(buf)));
@@ -564,10 +592,12 @@ impl OtaCertPatcher {
continue;
};
let mut zip = ZipArchive::new(Cursor::new(&data))?;
let mut zip = ZipArchive::new(Cursor::new(&data)).map_err(Error::Zip)?;
for index in 0..zip.len() {
let zip_entry = zip.by_index(index)?;
let zip_entry = zip
.by_index(index)
.map_err(|e| Error::ZipEntryIndex(index, e))?;
if !zip_entry.name().ends_with(".x509.pem") {
debug!("Skipping invalid entry path: {}", zip_entry.name());
continue;
@@ -1100,7 +1130,7 @@ pub fn patch_boot_images<'a>(
names: &[&'a str],
open_input: impl Fn(&str) -> io::Result<Box<dyn ReadSeek>> + Sync,
open_output: impl Fn(&str) -> io::Result<Box<dyn WriteSeek>> + Sync,
key: &RsaPrivateKey,
key: &RsaSigningKey,
patchers: &[Box<dyn BootImagePatch + Sync>],
cancel_signal: &AtomicBool,
) -> Result<HashSet<&'a str>> {
@@ -1184,7 +1214,12 @@ pub fn patch_boot_images<'a>(
info.header.sign(key)?;
}
avb::write_appended_image(writer, &info.header, &mut info.footer, info.image_size)?;
avb::write_appended_image(
writer,
&info.header,
&mut info.footer,
Some(info.image_size),
)?;
Ok(())
})
+5 -5
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,13 +11,13 @@ use std::{
use memchr::memmem;
use rayon::iter::{IntoParallelIterator, ParallelIterator};
use rsa::RsaPrivateKey;
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,
@@ -111,7 +111,7 @@ pub fn patch_system_image(
input: &(dyn ReadSeekReopen + Sync),
output: &(dyn WriteSeekReopen + Sync),
certificate: &Certificate,
key: &RsaPrivateKey,
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).
@@ -129,7 +129,7 @@ pub fn patch_system_image(
return Err(Error::NoHashTreeDescriptor);
};
let num_chunks = util::div_ceil(footer.original_image_size, CHUNK_SIZE);
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)
@@ -207,7 +207,7 @@ pub fn patch_system_image(
}
let writer = output.reopen_boxed()?;
avb::write_appended_image(writer, &header, &mut footer, image_size)?;
avb::write_appended_image(writer, &header, &mut footer, Some(image_size))?;
let AppendedDescriptorMut::HashTree(descriptor) = header.appended_descriptor_mut()? else {
return Err(Error::NoHashTreeDescriptor);
-10
View File
@@ -49,16 +49,6 @@ pub fn parent_path(path: &Path) -> &Path {
Path::new(".")
}
/// Since Rust's built-in .div_ceil() is still nightly-only.
pub fn div_ceil<T: PrimInt>(dividend: T, divisor: T) -> T {
dividend / divisor
+ if dividend % divisor != T::zero() {
T::one()
} else {
T::zero()
}
}
/// Sort and merge overlapping intervals.
pub fn merge_overlapping<T>(sections: &[Range<T>]) -> Vec<Range<T>>
where
+116 -7
View File
@@ -14,6 +14,7 @@ use rsa::RsaPrivateKey;
use avbroot::{
self,
crypto::RsaSigningKey,
format::avb::{
self, AlgorithmType, AppendedDescriptorMut, AppendedDescriptorRef,
ChainPartitionDescriptor, Descriptor, Footer, HashDescriptor, HashTreeDescriptor, Header,
@@ -22,7 +23,7 @@ use avbroot::{
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",
@@ -32,7 +33,8 @@ 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 {
@@ -228,7 +230,7 @@ fn round_trip_appended_hash_image() {
assert_eq!(header.verify().unwrap().unwrap(), key.to_public_key());
// Write vbmeta structures.
avb::write_appended_image(&mut writer, &header, &mut footer, image_size).unwrap();
avb::write_appended_image(&mut writer, &header, &mut footer, Some(image_size)).unwrap();
let data = writer.into_inner();
// Verify checksum of the output.
@@ -254,8 +256,8 @@ fn round_trip_appended_hash_image() {
}
#[test]
fn round_trip_appended_hash_tree_image() {
let image_size = 28672;
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,
@@ -334,7 +336,114 @@ fn round_trip_appended_hash_tree_image() {
assert_eq!(header.verify().unwrap().unwrap(), key.to_public_key());
// Write vbmeta structures.
avb::write_appended_image(&mut writer, &header, &mut footer, image_size).unwrap();
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 data).unwrap();
// 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();
@@ -358,5 +467,5 @@ fn round_trip_appended_hash_tree_image() {
assert_eq!(new_header, header);
assert_eq!(new_footer, footer);
assert_eq!(new_image_size, image_size);
assert_eq!(new_image_size, 28672);
}
+4 -2
View File
@@ -7,6 +7,7 @@ use std::io::Cursor;
use avbroot::{
self,
crypto::RsaSigningKey,
format::{
avb::{AlgorithmType, Descriptor, HashDescriptor, Header},
bootimage::{
@@ -19,7 +20,7 @@ use avbroot::{
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",
@@ -29,7 +30,8 @@ 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(s: &str, max_len: usize) -> String {
-2
View File
@@ -62,8 +62,6 @@ include-workspace = true
bypass = [
# Copies of unmodified crashwrangler objects for old macOS versions.
{ name = "honggfuzz", allow-globs = ["honggfuzz/third_party/mac/CrashReport_*.o"] },
# Test files for liblzma's test suite
{ name = "liblzma-sys", allow-globs = ["xz/tests/compress_prepared_bcj_*"] },
]
[sources]
+1 -1
View File
@@ -15,7 +15,7 @@ clap = { version = "4.4.1", features = ["derive"] }
ctrlc = "3.4.0"
hex = { version = "0.4.3", features = ["serde"] }
ring = "0.17.0"
rsa = "0.9.6"
rsa = { version = "0.9.6", features = ["hazmat"] }
serde = { version = "1.0.188", features = ["derive"] }
tempfile = "3.8.0"
toml_edit = { version = "0.22.9", features = ["serde"] }
+20 -8
View File
@@ -12,6 +12,9 @@ security_patch_level = "2024-01-01"
# Google Pixel 7 Pro
# What's unique: init_boot (boot v4) + vendor_boot (vendor v4)
[profile.pixel_v4_gki]
vabc_algo = "Lz4"
[profile.pixel_v4_gki.partitions.boot]
avb.signed = true
data.type = "boot"
@@ -46,12 +49,15 @@ data.version = "vendor_v4"
data.ramdisks = [["otacerts", "first_stage", "dsu_key_dir"]]
[profile.pixel_v4_gki.hashes]
original = "24a0a62cc08b96563f4872aee2fdd4a84d1a977b55c326dd9d4ddd92a1d326ea"
patched = "29889670efea78bace221742b19e8ace88b67137fc2f46dcd9dbdf67e4e42267"
original = "c00f891f941f3dddb28966f7b07f3acea773bee104dace82b37c2d1341f09422"
patched = "ce9d8ee97828d233809742a5d3f23aa27b042675b1935ca9e3df0592c55788fd"
# Google Pixel 6a
# What's unique: boot (boot v4, no ramdisk) + vendor_boot (vendor v4, 2 ramdisks)
[profile.pixel_v4_non_gki]
vabc_algo = "Lz4"
[profile.pixel_v4_non_gki.partitions.boot]
avb.signed = true
data.type = "boot"
@@ -80,12 +86,15 @@ data.version = "vendor_v4"
data.ramdisks = [["init", "otacerts", "first_stage", "dsu_key_dir"], ["dlkm"]]
[profile.pixel_v4_non_gki.hashes]
original = "0e7d0924a68d46e00abe96abfa0e5f3a98d5d15a32bef7401b91fca9a19748e8"
patched = "1a2f53d9ac3a1da75e5e7502110bda437c5a725764f16656c564d42460136279"
original = "4d692bc777b568b0626d3c08d2e6f83f1b472db5ad903486daaec6a78d0cc26e"
patched = "e27673e4f30933710c11d51f0e73849068cbe9bc9f54e6076bdd93f9a5c8ea0a"
# Google Pixel 4a 5G
# What's unique: boot (boot v3) + vendor_boot (vendor v3)
[profile.pixel_v3]
vabc_algo = "Lz4"
[profile.pixel_v3.partitions.boot]
avb.signed = true
data.type = "boot"
@@ -115,12 +124,15 @@ data.version = "vendor_v3"
data.ramdisks = [["otacerts", "first_stage", "dsu_key_dir"]]
[profile.pixel_v3.hashes]
original = "533e6f233cb98c98c945044c2ee81a6069e66baee6f7dbcfbf7523795a11215e"
patched = "1eeae9dba0302c2d469bd03c8bccdc0469c171204dbd676a20cb6620d2d11c5c"
original = "f432dc7931520feb238474aa707dd5299747562ffe6129f3f763b5f11ac473ab"
patched = "3850a2e73bd783a1ec4a70c59f37d2374e017c20df7ab4b591182b14d187c18e"
# Google Pixel 4a
# What's unique: boot (boot v2)
[profile.pixel_v2]
vabc_algo = "Gzip"
[profile.pixel_v2.partitions.boot]
avb.signed = false
data.type = "boot"
@@ -144,5 +156,5 @@ data.type = "vbmeta"
data.deps = ["system"]
[profile.pixel_v2.hashes]
original = "958dfa428abd2901178b90147903d7857ed78d6c016f6cb3af30d024a22a8f9a"
patched = "b0d18ef350ca7b6499b7de4b0182f4ac3be7288ad238ce888708643e750f7631"
original = "1b45235b58054009cc496f6c3ee11d3dc16ed5c388c861761e26a6fce83103a0"
patched = "193b2dc70dd465d686f35c7b7f74d2cc1b06a55e48cf5c2e4df0f667e03032fc"
+30 -5
View File
@@ -3,10 +3,11 @@
* SPDX-License-Identifier: GPL-3.0-only
*/
use std::path::PathBuf;
use std::{ffi::OsString, path::PathBuf};
use avbroot::cli::args::{LogFormat, LogLevel};
use clap::{Args, Parser, Subcommand};
use avbroot::cli::args::LogFormat;
use clap::{Args, Parser, Subcommand, ValueEnum};
use tracing::Level;
#[derive(Debug, Args)]
pub struct ProfileGroup {
@@ -69,10 +70,34 @@ pub struct Cli {
pub command: Command,
/// Lowest log message severity to output.
#[arg(long, global = true, value_name = "LEVEL", default_value_t)]
pub log_level: LogLevel,
#[arg(long, global = true, value_name = "LEVEL", default_value_t = Level::INFO)]
pub log_level: Level,
/// 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,
}
+2
View File
@@ -6,6 +6,7 @@
use std::{collections::BTreeMap, fs, path::Path};
use anyhow::{Context, Result};
use avbroot::format::payload::VabcAlgo;
use serde::{Deserialize, Serialize};
use toml_edit::DocumentMut;
@@ -109,6 +110,7 @@ pub struct Partition {
#[derive(Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Profile {
pub vabc_algo: Option<VabcAlgo>,
pub partitions: BTreeMap<String, Partition>,
pub hashes: Hashes,
}
+193 -56
View File
@@ -1,5 +1,5 @@
/*
* SPDX-FileCopyrightText: 2023 Andrew Gunnerson
* SPDX-FileCopyrightText: 2023-2024 Andrew Gunnerson
* SPDX-FileCopyrightText: 2023 Pascal Roeleven
* SPDX-License-Identifier: GPL-3.0-only
*/
@@ -9,10 +9,11 @@ mod config;
use std::{
collections::{BTreeMap, BTreeSet},
ffi::OsStr,
env,
ffi::{OsStr, OsString},
fs::{self, File},
io::{self, BufReader, BufWriter, Cursor, Seek, SeekFrom, Write},
path::Path,
io::{self, BufReader, BufWriter, Cursor, Read, Seek, SeekFrom, Write},
path::{Path, PathBuf},
sync::{
atomic::{AtomicBool, Ordering},
Arc,
@@ -22,7 +23,7 @@ use std::{
use anyhow::{anyhow, bail, Context, Result};
use avbroot::{
cli::ota::{ExtractCli, PatchCli, VerifyCli},
crypto::{self, PassphraseSource},
crypto::{self, PassphraseSource, RsaSigningKey},
format::{
avb::{
self, AlgorithmType, ChainPartitionDescriptor, Descriptor, Footer, HashDescriptor,
@@ -48,15 +49,15 @@ use avbroot::{
stream::{self, CountingWriter, HashingReader, PSeekFile, Reopen, ToWriter},
};
use clap::Parser;
use rsa::RsaPrivateKey;
use tempfile::{NamedTempFile, TempDir};
use rsa::{rand_core::OsRng, traits::PublicKeyParts, BigUint};
use tempfile::TempDir;
use topological_sort::TopologicalSort;
use tracing::{info, info_span};
use x509_cert::Certificate;
use zip::{write::FileOptions, CompressionMethod, ZipWriter};
use crate::{
cli::{Cli, Command, ListCli, ProfileGroup, TestCli},
cli::{Cli, Command, HelperCli, ListCli, PassSource, ProfileGroup, TestCli},
config::{
Avb, BootData, BootVersion, Config, Data, DmVerityContent, DmVerityData, OtaInfo,
Partition, Profile, RamdiskContent, VbmetaData,
@@ -100,7 +101,7 @@ fn append_avb(
avb: &Avb,
hash_tree: bool,
ota_info: &OtaInfo,
key_avb: &RsaPrivateKey,
key_avb: &RsaSigningKey,
cancel_signal: &AtomicBool,
) -> Result<()> {
let image_size = file.seek(SeekFrom::End(0))?;
@@ -196,7 +197,7 @@ fn append_avb(
// Give enough free space for changes from patching.
.max(1024 * 1024);
avb::write_appended_image(file, &header, &mut footer, full_image_size)?;
avb::write_appended_image(file, &header, &mut footer, Some(full_image_size))?;
Ok(())
}
@@ -298,7 +299,7 @@ fn create_boot_image(
avb: &Avb,
boot_data: &BootData,
ota_info: &OtaInfo,
key_avb: &RsaPrivateKey,
key_avb: &RsaSigningKey,
cert_ota: &Certificate,
cancel_signal: &AtomicBool,
) -> Result<()> {
@@ -425,7 +426,7 @@ fn create_dm_verity_image(
avb: &Avb,
dm_verity_data: &DmVerityData,
ota_info: &OtaInfo,
key_avb: &RsaPrivateKey,
key_avb: &RsaSigningKey,
cert_ota: &Certificate,
cancel_signal: &AtomicBool,
) -> Result<()> {
@@ -454,7 +455,7 @@ fn create_vbmeta_image(
avb: &Avb,
vbmeta_data: &VbmetaData,
inputs: &BTreeMap<String, PSeekFile>,
key: &RsaPrivateKey,
key: &RsaSigningKey,
) -> Result<()> {
let mut descriptors = Vec::new();
@@ -506,7 +507,7 @@ fn create_vbmeta_image(
fn create_partition_images(
partitions: &BTreeMap<String, Partition>,
ota_info: &OtaInfo,
key_avb: &RsaPrivateKey,
key_avb: &RsaSigningKey,
cert_ota: &Certificate,
cancel_signal: &AtomicBool,
) -> Result<BTreeMap<String, PSeekFile>> {
@@ -576,7 +577,8 @@ fn create_payload(
partitions: &BTreeMap<String, Partition>,
inputs: &BTreeMap<String, PSeekFile>,
ota_info: &OtaInfo,
key_ota: &RsaPrivateKey,
profile: &Profile,
key_ota: &RsaSigningKey,
cancel_signal: &AtomicBool,
) -> Result<(String, u64)> {
let dynamic_partitions_names = partitions
@@ -593,8 +595,14 @@ fn create_payload(
.map(PSeekFile::new)
.with_context(|| format!("Failed to create temp file for: {name}"))?;
let (partition_info, operations) =
payload::compress_image(file, &writer, name, 4096, cancel_signal)?;
let vabc_algo = if dynamic_partitions_names.contains(name) {
profile.vabc_algo
} else {
None
};
let (partition_info, operations, cow_estimate) =
payload::compress_image(file, &writer, name, 4096, vabc_algo, cancel_signal)?;
compressed.insert(name, writer);
@@ -617,7 +625,7 @@ fn create_payload(
fec_roots: None,
version: None,
merge_operations: vec![],
estimate_cow_size: None,
estimate_cow_size: cow_estimate,
});
}
@@ -638,7 +646,7 @@ fn create_payload(
}],
snapshot_enabled: Some(true),
vabc_enabled: Some(true),
vabc_compression_param: Some("gz".to_owned()),
vabc_compression_param: profile.vabc_algo.map(|a| a.to_string()),
cow_version: Some(2),
vabc_feature_set: None,
}),
@@ -681,7 +689,7 @@ fn create_payload(
.with_context(|| format!("Failed to copy from image: {name}"))?;
}
let (_, properties, metadata_size) = payload_writer
let (_, _, properties, metadata_size) = payload_writer
.finish()
.context("Failed to finalize payload")?;
@@ -692,8 +700,8 @@ fn create_ota(
output: &Path,
ota_info: &OtaInfo,
profile: &Profile,
key_avb: &RsaPrivateKey,
key_ota: &RsaPrivateKey,
key_avb: &RsaSigningKey,
key_ota: &RsaSigningKey,
cert_ota: &Certificate,
cancel_signal: &AtomicBool,
) -> Result<()> {
@@ -739,6 +747,7 @@ fn create_ota(
&profile.partitions,
&inputs,
ota_info,
profile,
key_ota,
cancel_signal,
)
@@ -823,12 +832,16 @@ fn create_fake_magisk(output: &Path) -> Result<()> {
for path in [
"assets/stub.apk",
"lib/arm64-v8a/libinit-ld.so",
"lib/arm64-v8a/libmagisk64.so",
"lib/arm64-v8a/libmagiskinit.so",
"lib/armeabi-v7a/libinit-ld.so",
"lib/armeabi-v7a/libmagisk32.so",
"lib/armeabi-v7a/libmagiskinit.so",
"lib/x86/libinit-ld.so",
"lib/x86/libmagisk32.so",
"lib/x86/libmagiskinit.so",
"lib/x86_64/libinit-ld.so",
"lib/x86_64/libmagisk64.so",
"lib/x86_64/libmagiskinit.so",
] {
@@ -844,15 +857,18 @@ fn create_fake_magisk(output: &Path) -> Result<()> {
}
struct KeySet {
avb_key: RsaPrivateKey,
ota_key: RsaPrivateKey,
avb_key: RsaSigningKey,
ota_key: RsaSigningKey,
ota_cert: Certificate,
avb_key_file: NamedTempFile,
avb_pass_file: NamedTempFile,
avb_pkmd_file: NamedTempFile,
ota_key_file: NamedTempFile,
ota_pass_file: NamedTempFile,
ota_cert_file: NamedTempFile,
_key_dir: TempDir,
avb_key_file: PathBuf,
avb_public_key_file: PathBuf,
avb_pass_file: PathBuf,
avb_pkmd_file: PathBuf,
ota_key_file: PathBuf,
ota_public_key_file: PathBuf,
ota_pass_file: PathBuf,
ota_cert_file: PathBuf,
}
macro_rules! new_keys_with_prefix {
@@ -901,13 +917,8 @@ macro_rules! new_keys_with_prefix {
}
impl KeySet {
fn write_temp(data: &[u8]) -> Result<NamedTempFile> {
let mut temp_file =
NamedTempFile::new().context("Failed to create temp file for test keys")?;
temp_file
.write_all(data)
.with_context(|| format!("Failed to write test key data: {:?}", temp_file.path()))?;
Ok(temp_file)
fn write(path: &Path, data: &[u8]) -> Result<()> {
fs::write(path, data).with_context(|| format!("Failed to write test key data: {path:?}"))
}
fn new_with_data(
@@ -918,36 +929,54 @@ impl KeySet {
ota_pass: &[u8],
ota_cert: &[u8],
) -> Result<Self> {
let avb_key_file = Self::write_temp(avb_key)?;
let avb_pass_file = Self::write_temp(avb_pass)?;
let avb_pkmd_file = Self::write_temp(avb_pkmd)?;
let ota_key_file = Self::write_temp(ota_key)?;
let ota_pass_file = Self::write_temp(ota_pass)?;
let ota_cert_file = Self::write_temp(ota_cert)?;
let key_dir = TempDir::new().context("Failed to create temp directory")?;
let avb_key_file = key_dir.path().join("avb.key");
let avb_public_key_file = key_dir.path().join("avb.public.key");
let avb_pass_file = key_dir.path().join("avb.passphrase");
let avb_pkmd_file = key_dir.path().join("avb_pkmd.bin");
let ota_key_file = key_dir.path().join("ota.key");
let ota_public_key_file = key_dir.path().join("ota.public.key");
let ota_pass_file = key_dir.path().join("ota.passphrase");
let ota_cert_file = key_dir.path().join("ota.crt");
Self::write(&avb_key_file, avb_key)?;
Self::write(&avb_pass_file, avb_pass)?;
Self::write(&avb_pkmd_file, avb_pkmd)?;
Self::write(&ota_key_file, ota_key)?;
Self::write(&ota_pass_file, ota_pass)?;
Self::write(&ota_cert_file, ota_cert)?;
let avb_key = crypto::read_pem_key_file(
avb_key_file.path(),
&PassphraseSource::File(avb_pass_file.path().to_owned()),
&avb_key_file,
&PassphraseSource::File(avb_pass_file.clone()),
)
.map(RsaSigningKey::Internal)
.context("Failed to load AVB test key")?;
let ota_key = crypto::read_pem_key_file(
ota_key_file.path(),
&PassphraseSource::File(ota_pass_file.path().to_owned()),
&ota_key_file,
&PassphraseSource::File(ota_pass_file.clone()),
)
.map(RsaSigningKey::Internal)
.context("Failed to load OTA test key")?;
let ota_cert = crypto::read_pem_cert_file(ota_cert_file.path())
.context("Failed to load OTA test cert")?;
crypto::write_pem_public_key_file(&avb_public_key_file, &avb_key.to_public_key())?;
crypto::write_pem_public_key_file(&ota_public_key_file, &ota_key.to_public_key())?;
let ota_cert =
crypto::read_pem_cert_file(&ota_cert_file).context("Failed to load OTA test cert")?;
Ok(Self {
avb_key,
ota_key,
ota_cert,
_key_dir: key_dir,
avb_key_file,
avb_public_key_file,
avb_pass_file,
avb_pkmd_file,
ota_key_file,
ota_public_key_file,
ota_pass_file,
ota_cert_file,
})
@@ -960,12 +989,20 @@ impl KeySet {
fn patch_image(
input_file: &Path,
output_file: &Path,
system_image_file: &Path,
extra_args: &[&OsStr],
keys: &KeySet,
signing_helper: bool,
cancel_signal: &AtomicBool,
) -> Result<()> {
info!("Patching OTA: {input_file:?} -> {output_file:?}");
let (avb_key_file, ota_key_file) = if signing_helper {
(&keys.avb_public_key_file, &keys.ota_public_key_file)
} else {
(&keys.avb_key_file, &keys.ota_key_file)
};
// We're intentionally using the CLI interface.
let mut args: Vec<&OsStr> = vec![
OsStr::new("patch"),
@@ -973,18 +1010,29 @@ fn patch_image(
input_file.as_os_str(),
OsStr::new("--output"),
output_file.as_os_str(),
OsStr::new("--replace"),
OsStr::new("system"),
system_image_file.as_os_str(),
OsStr::new("--key-avb"),
keys.avb_key_file.path().as_os_str(),
avb_key_file.as_os_str(),
OsStr::new("--pass-avb-file"),
keys.avb_pass_file.path().as_os_str(),
keys.avb_pass_file.as_os_str(),
OsStr::new("--key-ota"),
keys.ota_key_file.path().as_os_str(),
ota_key_file.as_os_str(),
OsStr::new("--pass-ota-file"),
keys.ota_pass_file.path().as_os_str(),
keys.ota_pass_file.as_os_str(),
OsStr::new("--cert-ota"),
keys.ota_cert_file.path().as_os_str(),
keys.ota_cert_file.as_os_str(),
OsStr::new("--dsu"),
];
let argv0: OsString;
if signing_helper {
argv0 = env::args_os().next().unwrap();
args.push(OsStr::new("--signing-helper"));
args.push(&argv0);
}
args.extend_from_slice(extra_args);
let cli = PatchCli::try_parse_from(args)?;
@@ -1016,9 +1064,9 @@ fn verify_image(input_file: &Path, keys: &KeySet, cancel_signal: &AtomicBool) ->
OsStr::new("--input"),
input_file.as_os_str(),
OsStr::new("--public-key-avb"),
keys.avb_pkmd_file.path().as_os_str(),
keys.avb_pkmd_file.as_os_str(),
OsStr::new("--cert-ota"),
keys.ota_cert_file.path().as_os_str(),
keys.ota_cert_file.as_os_str(),
])?;
avbroot::cli::ota::verify_subcommand(&cli, cancel_signal)?;
@@ -1119,11 +1167,18 @@ fn test_subcommand(cli: &TestCli, cancel_signal: &AtomicBool) -> Result<()> {
.with_context(|| format!("[{name}] Failed to verify original OTA hash"))?;
// Patch once using Magisk.
extract_image(&out_original, &profile_dir, cancel_signal)
.with_context(|| format!("[{name}] Failed to extract OTA"))?;
let system_image = profile_dir.join("system.img");
patch_image(
&out_original,
&out_magisk,
&system_image,
&args_magisk,
&test_keys,
false,
cancel_signal,
)
.with_context(|| format!("[{name}] Failed to patch OTA"))?;
@@ -1149,8 +1204,10 @@ fn test_subcommand(cli: &TestCli, cancel_signal: &AtomicBool) -> Result<()> {
patch_image(
&out_original,
&out_prepatched,
&system_image,
&args_prepatched,
&test_keys,
true,
cancel_signal,
)
.with_context(|| format!("[{name}] Failed to patch OTA"))?;
@@ -1175,7 +1232,87 @@ fn list_subcommand(cli: &ListCli) -> Result<()> {
Ok(())
}
/// A basic --signing-helper implementation that just signs via RustCrypto.
fn helper_mode() -> Result<()> {
let cli = HelperCli::parse();
let private_key_path = {
let parent = cli.public_key.parent().unwrap_or(Path::new("."));
let name = cli
.public_key
.file_name()
.and_then(|n| n.to_str())
.ok_or_else(|| anyhow!("Bad filename: {:?}", cli.public_key))?;
parent.join(name.replace(".public", ""))
};
let source = match cli.pass_source {
PassSource::Env => PassphraseSource::EnvVar(cli.pass_source_value),
PassSource::File => PassphraseSource::File(cli.pass_source_value.into()),
};
let private_key = crypto::read_pem_key_file(&private_key_path, &source)
.with_context(|| format!("Failed to load private key: {private_key_path:?}"))?;
let public_key = crypto::read_pem_public_key_file(&cli.public_key)
.with_context(|| format!("Failed to load public key: {:?}", cli.public_key))?;
if private_key.to_public_key() != public_key {
bail!("Private key does not match public key");
}
let (hash_algo, key_algo) = cli
.algorithm
.split_once('_')
.ok_or_else(|| anyhow!("Unknown algorithm: {:?}", cli.algorithm))?;
if key_algo != format!("RSA{}", private_key.size() * 8) {
bail!(
"{key_algo} does not match key size ({})",
private_key.size() * 8
);
} else if hash_algo != "SHA256" && hash_algo != "SHA512" {
bail!("Unknown hash algorithm: {hash_algo}");
}
let mut padded_digest = vec![];
io::stdin()
.read_to_end(&mut padded_digest)
.context("Failed to read padded digest from stdin")?;
if padded_digest.len() != private_key.size() {
bail!(
"Padded digest size ({}) bytes does not match key size ({})",
padded_digest.len(),
private_key.size()
);
}
// The input is already padded, so perform a raw RSA signing operation.
let mut signature = rsa::hazmat::rsa_decrypt_and_check(
&private_key,
None::<&mut OsRng>,
&BigUint::from_bytes_be(&padded_digest),
)
.context("Failed to sign digest")?
.to_bytes_le();
signature.resize(private_key.size(), 0);
signature.reverse();
io::stdout()
.write_all(&signature)
.context("Failed to write signature to stdout")?;
Ok(())
}
fn main() -> Result<()> {
const ENV_HELPER_MODE: &str = "E2E_HELPER_MODE";
// Re-invoking ourselves will execute as the helper script instead.
if env::var_os(ENV_HELPER_MODE).is_some() {
return helper_mode();
}
env::set_var(ENV_HELPER_MODE, "true");
// Set up a cancel signal so we can properly clean up any temporary files.
let cancel_signal = Arc::new(AtomicBool::new(false));
{