mirror of
https://github.com/chenxiaolong/avbroot.git
synced 2026-07-03 14:05:11 +02:00
Compare commits
101 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0cdc7172dd | |||
| 1296275418 | |||
| a62d0a5c91 | |||
| de433a2724 | |||
| c08af33343 | |||
| e224884591 | |||
| a525fc4550 | |||
| 8c6b0fdfc5 | |||
| 36d4ed19ad | |||
| f9b06b33e6 | |||
| 620c873be5 | |||
| e929ecbe44 | |||
| 31685713ef | |||
| 0dab7aa32c | |||
| ba6c1b1400 | |||
| 1ecbf1144d | |||
| 84fa6c6bc6 | |||
| 15b7db4631 | |||
| 1d9c1574da | |||
| 43db728b9d | |||
| fbe9f629ab | |||
| 80d5f19223 | |||
| cff5ac6b2e | |||
| 2e3b5db9fe | |||
| 58a279f3a4 | |||
| 1484cd47c3 | |||
| 2ee38b716b | |||
| d4eb231dd4 | |||
| cf064e145d | |||
| 3b0c97a93d | |||
| 2d4f08f48b | |||
| a753304dff | |||
| 9480e2ffa4 | |||
| 140d0ddd8b | |||
| e0b114cf71 | |||
| 78449f686d | |||
| a2fb807803 | |||
| 6de5cb783a | |||
| 12d6f7f78c | |||
| 3c0a77df21 | |||
| d1b6bce227 | |||
| 10e0748d18 | |||
| 9cf63c7036 | |||
| 291b3c887b | |||
| 5d7eb13fbc | |||
| 05cd74719e | |||
| 7e0d5584d9 | |||
| b351d47f28 | |||
| 421b0a5207 | |||
| cad08a6a2f | |||
| 0a32dfaec3 | |||
| b03b2ade6a | |||
| c198646c7c | |||
| d815a78e9a | |||
| 99c5800f96 | |||
| c801fbb069 | |||
| 0b250086a3 | |||
| f6c6c8ab40 | |||
| 5c353a5f42 | |||
| ad932ed445 | |||
| d04d5451dc | |||
| 1d5db9f731 | |||
| 409b9298a5 | |||
| 516238d907 | |||
| 39d5fbf76d | |||
| 20f5bdacc7 | |||
| 5b8eefa867 | |||
| b2610fa0e9 | |||
| 8a5550a43c | |||
| e16f4c7fa7 | |||
| 983e6c40a5 | |||
| 8729854727 | |||
| 0d1beb7734 | |||
| 2cd0d69238 | |||
| c0e65264e5 | |||
| 37b15a2b6a | |||
| e4994fbe98 | |||
| cf1bacab30 | |||
| 4832121160 | |||
| c614e61744 | |||
| 9ebce1666c | |||
| fb34198ffc | |||
| 4a5eab4ba0 | |||
| 1e1818ad8f | |||
| 3f25ad7c76 | |||
| 37e28eb040 | |||
| ce87757fd1 | |||
| 10c425dede | |||
| 8a0d147993 | |||
| 062aa21485 | |||
| 7ffeb5e5cb | |||
| a9a1aa55e9 | |||
| 0ca2872111 | |||
| a403c26b54 | |||
| 4afa1d1ba6 | |||
| 9f669bc53e | |||
| fc89159f09 | |||
| 18f43a0e57 | |||
| af7262a0d2 | |||
| 5a7885674e | |||
| c974ab5ec3 |
@@ -20,6 +20,8 @@ jobs:
|
||||
RUSTFLAGS: -C strip=symbols -C target-feature=+crt-static
|
||||
TARGETS: ${{ join(matrix.artifact.targets, ' ') || matrix.artifact.name }}
|
||||
ANDROID_API: ${{ matrix.artifact.android_api }}
|
||||
# https://aws.github.io/aws-lc-rs/requirements/windows.html#use-of-prebuilt-nasm-objects
|
||||
AWS_LC_SYS_PREBUILT_NASM: 1
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
@@ -42,7 +44,7 @@ jobs:
|
||||
android_api: '31'
|
||||
steps:
|
||||
- name: Check out repository
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
# For git describe
|
||||
fetch-depth: 0
|
||||
@@ -65,7 +67,7 @@ jobs:
|
||||
run: |
|
||||
cargo install \
|
||||
--git https://github.com/chenxiaolong/cargo-android \
|
||||
--tag v0.1.1
|
||||
--tag v0.1.3
|
||||
|
||||
- name: Get version
|
||||
id: get_version
|
||||
@@ -84,7 +86,9 @@ jobs:
|
||||
done
|
||||
|
||||
- name: Cache Rust dependencies
|
||||
uses: Swatinem/rust-cache@v2
|
||||
uses: Swatinem/rust-cache@f0deed1e0edfc6a9be95417288c0e1099b1eeec3 # v2.7.7
|
||||
with:
|
||||
key: ${{ matrix.artifact.name }}
|
||||
|
||||
- name: Clippy
|
||||
shell: bash
|
||||
@@ -153,7 +157,7 @@ jobs:
|
||||
run: cp LICENSE README.md target/output/
|
||||
|
||||
- name: Archive executable
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4.6.1
|
||||
with:
|
||||
name: avbroot-${{ steps.get_version.outputs.version }}-${{ matrix.artifact.name }}
|
||||
path: |
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
---
|
||||
name: cargo-deny
|
||||
on:
|
||||
push:
|
||||
@@ -11,7 +10,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out repository
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
|
||||
- name: Run cargo-deny
|
||||
uses: EmbarkStudios/cargo-deny-action@v1
|
||||
uses: EmbarkStudios/cargo-deny-action@34899fc7ba81ca6268d5947a7a16b4649013fea1 # v2.0.11
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
---
|
||||
name: Github Release
|
||||
on:
|
||||
push:
|
||||
@@ -25,10 +24,10 @@ jobs:
|
||||
echo "version=${version}" >> "${GITHUB_OUTPUT}"
|
||||
|
||||
- name: Check out repository
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
|
||||
- name: Create release
|
||||
uses: softprops/action-gh-release@v2
|
||||
uses: softprops/action-gh-release@c95fe1489396fe8a9eb87c0abf8aa5b2ef267fda # v2.2.1
|
||||
with:
|
||||
tag_name: v${{ steps.get_version.outputs.version }}
|
||||
name: Version ${{ steps.get_version.outputs.version }}
|
||||
|
||||
+102
@@ -7,6 +7,67 @@
|
||||
to update the actual links at the bottom of the file.
|
||||
-->
|
||||
|
||||
### Version 3.13.0
|
||||
|
||||
* Fix parsing Samsung `super.img` files in `avbroot lp` due to Samsung putting their own data structures in a region that's supposed to be filled with zeros ([PR #415])
|
||||
* Add advanced option to skip replacing the OTA certificate in the system image ([Discussion #417], [PR #418])
|
||||
* Switch to stable bzip2-rs release and use zlib-rs as the backend for flate2 ([PR #421])
|
||||
* Switch to the aws-lc cryptography library for SHA1 and SHA2 hashing ([PR #422])
|
||||
* The ring library is no longer maintained
|
||||
* Fix incorrect `Partitions aren't protected by AVB: system` warning when using `--skip-system-ota-cert` ([PR #423])
|
||||
* Discard unneeded temp file sooner when using `--skip-system-ota-cert` ([PR #424])
|
||||
* Make `avbroot lp`'s parser less strict so that it can load on-device `super` partitions ([PR #425])
|
||||
* The on-disk layout on virtual A/B devices violates some requirements stated in AOSP's documentation
|
||||
* Update dependencies ([PR #427])
|
||||
|
||||
### Version 3.12.0
|
||||
|
||||
* Add new `-p <name>` option to `avbroot ota extract` for extracting specific partitions ([PR #408])
|
||||
* Deprecate the `--boot-only` option in `avbroot ota extract` ([PR #408])
|
||||
* The option will remain indefinitely for backwards compatibility, but is hidden from `--help`
|
||||
* Add support for extracting the embedded OTA certificate and AVB public key in `avbroot ota extract` ([PR #409])
|
||||
* Rename `avbroot key extract-avb` to `avbroot key encode-avb` for consistency with `avbroot key decode-avb` ([PR #410])
|
||||
* The old syntax will remain supported indefinitely for backwards compatibility, but is hidden from `--help`
|
||||
* Update dependencies ([PR #411])
|
||||
|
||||
### Version 3.11.0
|
||||
|
||||
* Fix crash when ignoring warning about `--magisk-preinit-device` not being specified ([PR #394])
|
||||
* When using `--ignore-magisk-warnings`, assume that unsupported Magisk versions newer than the latest supported version are capable of all features ([Issue #393], [PR #395])
|
||||
* Update bzip2-rs and switch to the Rust backend ([PR #397], [PR #402])
|
||||
* Minor code cleanup for custom integer range type ([PR #398])
|
||||
* Improve errors to make them less ambiguous about what went wrong ([PR #401])
|
||||
* Fix bug where a vendor v4 boot image that was truncated in the bootconfig padding section would be accepted as valid ([PR #401])
|
||||
* Avoid performing many small I/O operations when reading and writing cpio archives ([PR #403])
|
||||
* Update dependencies ([PR #404])
|
||||
|
||||
### Version 3.10.0
|
||||
|
||||
* Switch to using zerocopy library for all binary file format parsers ([PR #384])
|
||||
* Update to latest AOSP protobuf schema for the `payload.bin` metadata file format ([PR #385])
|
||||
* Update dependencies and pin Github Actions actions to specific commits ([PR #386], [PR #392])
|
||||
* Improve error messages from file format parsers ([PR #390])
|
||||
* Add support for Magisk 28100 ([PR #391])
|
||||
|
||||
### Version 3.9.0
|
||||
|
||||
* Update all dependencies ([PR #368], [PR #377])
|
||||
* Add advanced option to skip replacing the OTA certificate in the recovery image ([Issue #366], [PR #367], [PR #371])
|
||||
* Improve error message when an incompatible RSA key is used for AVB signing ([Issue #366], [PR #369])
|
||||
* Fix clippy warnings ([PR #370])
|
||||
* Allow `avbroot ota verify` to verify OTAs that lack `META-INF/com/android/metadata.pb` ([Issue #366], [PR #373])
|
||||
* Allow `avbroot ota verify` to verify OTAs where the payload signature does not set `unpadded_signature_size` ([Issue #366], [PR #374])
|
||||
* Allow `avbroot sparse` to parse sparse images with unknown fields (matches AOSP implementation) ([PR #376])
|
||||
|
||||
### Version 3.8.0
|
||||
|
||||
* Add `avbroot avb digest` subcommand for computing the special vbmeta digest ([PR #363])
|
||||
* Update all dependencies ([PR #364])
|
||||
|
||||
### Version 3.7.1
|
||||
|
||||
* Add support for Magisk 28000 ([PR #362])
|
||||
|
||||
### Version 3.7.0
|
||||
|
||||
* Fix a nasty regression since version 2.0.0 where recovery mode's `otacerts.zip` modifications were lost when using `--prepatched` with Magisk on some older devices, like the Pixel 4a ([Issue #356], [PR #357])
|
||||
@@ -232,6 +293,7 @@ Behind-the-scenes changes:
|
||||
[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
|
||||
[Discussion #417]: https://github.com/chenxiaolong/avbroot/discussions/417
|
||||
[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
|
||||
@@ -256,6 +318,8 @@ Behind-the-scenes changes:
|
||||
[Issue #328]: https://github.com/chenxiaolong/avbroot/issues/328
|
||||
[Issue #332]: https://github.com/chenxiaolong/avbroot/issues/332
|
||||
[Issue #356]: https://github.com/chenxiaolong/avbroot/issues/356
|
||||
[Issue #366]: https://github.com/chenxiaolong/avbroot/issues/366
|
||||
[Issue #393]: https://github.com/chenxiaolong/avbroot/issues/393
|
||||
[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
|
||||
@@ -364,3 +428,41 @@ Behind-the-scenes changes:
|
||||
[PR #354]: https://github.com/chenxiaolong/avbroot/pull/354
|
||||
[PR #355]: https://github.com/chenxiaolong/avbroot/pull/355
|
||||
[PR #357]: https://github.com/chenxiaolong/avbroot/pull/357
|
||||
[PR #362]: https://github.com/chenxiaolong/avbroot/pull/362
|
||||
[PR #363]: https://github.com/chenxiaolong/avbroot/pull/363
|
||||
[PR #364]: https://github.com/chenxiaolong/avbroot/pull/364
|
||||
[PR #367]: https://github.com/chenxiaolong/avbroot/pull/367
|
||||
[PR #368]: https://github.com/chenxiaolong/avbroot/pull/368
|
||||
[PR #369]: https://github.com/chenxiaolong/avbroot/pull/369
|
||||
[PR #370]: https://github.com/chenxiaolong/avbroot/pull/370
|
||||
[PR #371]: https://github.com/chenxiaolong/avbroot/pull/371
|
||||
[PR #373]: https://github.com/chenxiaolong/avbroot/pull/373
|
||||
[PR #374]: https://github.com/chenxiaolong/avbroot/pull/374
|
||||
[PR #376]: https://github.com/chenxiaolong/avbroot/pull/376
|
||||
[PR #377]: https://github.com/chenxiaolong/avbroot/pull/377
|
||||
[PR #384]: https://github.com/chenxiaolong/avbroot/pull/384
|
||||
[PR #385]: https://github.com/chenxiaolong/avbroot/pull/385
|
||||
[PR #386]: https://github.com/chenxiaolong/avbroot/pull/386
|
||||
[PR #390]: https://github.com/chenxiaolong/avbroot/pull/390
|
||||
[PR #391]: https://github.com/chenxiaolong/avbroot/pull/391
|
||||
[PR #392]: https://github.com/chenxiaolong/avbroot/pull/392
|
||||
[PR #394]: https://github.com/chenxiaolong/avbroot/pull/394
|
||||
[PR #395]: https://github.com/chenxiaolong/avbroot/pull/395
|
||||
[PR #397]: https://github.com/chenxiaolong/avbroot/pull/397
|
||||
[PR #398]: https://github.com/chenxiaolong/avbroot/pull/398
|
||||
[PR #401]: https://github.com/chenxiaolong/avbroot/pull/401
|
||||
[PR #402]: https://github.com/chenxiaolong/avbroot/pull/402
|
||||
[PR #403]: https://github.com/chenxiaolong/avbroot/pull/403
|
||||
[PR #404]: https://github.com/chenxiaolong/avbroot/pull/404
|
||||
[PR #408]: https://github.com/chenxiaolong/avbroot/pull/408
|
||||
[PR #409]: https://github.com/chenxiaolong/avbroot/pull/409
|
||||
[PR #410]: https://github.com/chenxiaolong/avbroot/pull/410
|
||||
[PR #411]: https://github.com/chenxiaolong/avbroot/pull/411
|
||||
[PR #415]: https://github.com/chenxiaolong/avbroot/pull/415
|
||||
[PR #418]: https://github.com/chenxiaolong/avbroot/pull/418
|
||||
[PR #421]: https://github.com/chenxiaolong/avbroot/pull/421
|
||||
[PR #422]: https://github.com/chenxiaolong/avbroot/pull/422
|
||||
[PR #423]: https://github.com/chenxiaolong/avbroot/pull/423
|
||||
[PR #424]: https://github.com/chenxiaolong/avbroot/pull/424
|
||||
[PR #425]: https://github.com/chenxiaolong/avbroot/pull/425
|
||||
[PR #427]: https://github.com/chenxiaolong/avbroot/pull/427
|
||||
|
||||
Generated
+524
-356
File diff suppressed because it is too large
Load Diff
+9
-1
@@ -4,7 +4,15 @@ members = ["avbroot", "e2e", "fuzz", "xtask"]
|
||||
resolver = "2"
|
||||
|
||||
[workspace.package]
|
||||
version = "3.7.0"
|
||||
version = "3.13.0"
|
||||
license = "GPL-3.0-only"
|
||||
edition = "2021"
|
||||
repository = "https://github.com/chenxiaolong/avbroot"
|
||||
|
||||
[workspace.lints.clippy]
|
||||
cast_lossless = "deny"
|
||||
missing_fields_in_debug = "warn"
|
||||
redundant_clone = "deny"
|
||||
|
||||
[workspace.lints.rust]
|
||||
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(fuzzing)'] }
|
||||
|
||||
@@ -66,6 +66,16 @@ If `-p` is omitted, the signatures and hashes are checked only for validity, not
|
||||
|
||||
By default, this command will not write to any file and fails if an image is corrupt or invalid. To attempt to repair corrupted dm-verity images, pass in `--repair`.
|
||||
|
||||
### Computing vbmeta digest
|
||||
|
||||
```bash
|
||||
avbroot avb digest -i <root vbmeta image>
|
||||
```
|
||||
|
||||
This subcommand computes the vbmeta digest, which is defined as the SHA256 digest of the root vbmeta partition's header, followed by the chained partitions' headers (if any) in the order that they are listed. Chained partitions more than one level deep are ignored.
|
||||
|
||||
This digest is equal to the value of the `ro.boot.vbmeta.digest` property or the `RootOfTrust.verifiedBootHash` hardware attestation field.
|
||||
|
||||
## `avbroot boot`
|
||||
|
||||
### Unpacking a boot image
|
||||
|
||||
@@ -121,7 +121,7 @@ When patching OTAs for multiple devices, generating unique keys for each device
|
||||
2. Convert the public key portion of the AVB signing key to the AVB public key metadata format. This is the format that the bootloader requires when setting the custom root of trust.
|
||||
|
||||
```bash
|
||||
avbroot key extract-avb -k avb.key -o avb_pkmd.bin
|
||||
avbroot key encode-avb -k avb.key -o avb_pkmd.bin
|
||||
```
|
||||
|
||||
3. Generate a self-signed certificate for the OTA signing key. This is used by recovery to verify OTA updates when sideloading.
|
||||
@@ -297,8 +297,7 @@ Magisk versions 25211 and newer require a writable partition for storing custom
|
||||
```bash
|
||||
avbroot ota extract \
|
||||
--input /path/to/ota.zip \
|
||||
--directory . \
|
||||
--boot-only
|
||||
--partition <name> # init_boot or boot, depending on device
|
||||
```
|
||||
|
||||
2. Patch the boot image via the Magisk app. This **MUST** be done on the target device or a device of the same model! The partition name will be incorrect if patched from Magisk on a different device model.
|
||||
@@ -385,6 +384,29 @@ Note that avbroot will validate that the prepatched image is compatible with the
|
||||
|
||||
avbroot can be used for just re-signing an OTA by specifying `--rootless` instead of `--magisk`/`--prepatched`. With this option, the patched OTA will not be rooted. The only modification applied is the replacement of the OTA verification certificate so that the OS can be upgraded with future (patched) OTAs.
|
||||
|
||||
### Skipping OTA certificate patches
|
||||
|
||||
avbroot can skip modifying `otacerts.zip` with the `--skip-system-ota-cert` and `--skip-recovery-ota-cert` options. **Do not use these unless you have a good reason to do so.**
|
||||
|
||||
When `--skip-system-ota-cert` is used, the OTA certificates in the `system` partition will not be modified. This prevents custom OTA updater apps from installing further patched OTAs while booted into Android.
|
||||
|
||||
When `--skip-recovery-ota-cert` is used, the OTA certificates in the `vendor_boot` or `recovery` partition will not be modified. **This prevents sideloading further patched OTAs from recovery mode.**
|
||||
|
||||
If `--skip-recovery-ota-cert` is used because the OTA certificate was already manually added to the boot image, then [verifying the patched OTA](#verifying-otas) afterwards is recommended to ensure that it was properly done. The verification process is only capable of checking the boot image's copy of the OTA certificates, not the system image's copy of them.
|
||||
|
||||
### Skipping all patches
|
||||
|
||||
To have avbroot make the absolute minimal changes:
|
||||
|
||||
* Specify `--skip-system-ota-cert`
|
||||
* Specify `--skip-recovery-ota-cert`
|
||||
* Specify `--rootless`
|
||||
* Omit `--dsu`
|
||||
|
||||
This will re-sign the `vbmeta` partition and the OTA with the custom keys, but leave all other partitions untouched.
|
||||
|
||||
**This should only be used for advanced troubleshooting.** Without the OTA certificate patches, the resulting OTA will not be able to install further updates.
|
||||
|
||||
### Replacing partitions
|
||||
|
||||
avbroot supports replacing entire partitions in the OTA, even partitions that are not boot images (eg. `vendor_dlkm`). A partition can be replaced by passing in `--replace <partition name> /path/to/partition.img`.
|
||||
@@ -445,17 +467,20 @@ avbroot prompts for the private key passphrases interactively by default. To run
|
||||
|
||||
* Use unencrypted private keys. This is strongly discouraged.
|
||||
|
||||
### Extracting the entire OTA
|
||||
### Extracting an OTA
|
||||
|
||||
To extract all images contained within the OTA's `payload.bin`, run:
|
||||
To extract the partition images contained within an OTA's `payload.bin`, run:
|
||||
|
||||
```bash
|
||||
avbroot ota extract \
|
||||
--input /path/to/ota.zip \
|
||||
--directory extracted \
|
||||
--all
|
||||
--directory extracted
|
||||
```
|
||||
|
||||
By default, this only extracts the images that could potentially be patched by avbroot. To extract all images, use the `--all` option. To extract specific images, use the `--partition <name>` option, which can be specified multiple times.
|
||||
|
||||
This command also supports extracting the embedded OTA certificate and AVB public key using the `--cert-ota` and `--public-key-avb` options. To extract only these components, pass in `--none` to skip extracting partition images.
|
||||
|
||||
### Zip write mode
|
||||
|
||||
By default, avbroot uses streaming writes for the output OTA during patching. This means it computes the sha256 digest for the digital signature as the file is being written. This mode causes the zip file to contain data descriptors, which is part of the zip standard and works on the vast majority of devices. However, some devices may have broken zip file parsers and fail to properly read OTA zip files containing data descriptors. If this is the case, pass in `--zip-mode seekable` when patching.
|
||||
@@ -484,6 +509,57 @@ By default, this behavior is compatible with the `--signing_helper` option in AO
|
||||
|
||||
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.
|
||||
|
||||
### 16K page size developer option
|
||||
|
||||
On recent devices running Android 16 and newer, there may be an option in Android's developer options to switch to a 16K page size kernel. This will not work when running an avbroot-patched OS. The switch internally works by flashing incremental OTAs:
|
||||
|
||||
* `/vendor/boot_otas/boot_ota_16k.zip` to switch to the 16K page size kernel (requires the `boot` partition to be currently flashed with the 4K kernel)
|
||||
* `/vendor/boot_otas/boot_ota_4k.zip` to switch to the 4K page size kernel (requires the `boot` partition to be currently flashed with the 16K kernel)
|
||||
|
||||
These `boot_otas` are unflashable when running an avbroot-patched OS because the `payload.bin` inside of them are signed by the OEM's key. These are also not proper OTA files. They don't contain any OTA metadata and the zip file itself is not signed. It's nothing more than a plain old zip file that stores a signed `payload.bin`.
|
||||
|
||||
There are no plans to add support for patching these `boot_otas`. It requires support for modifying filesystems and handling incremental OTAs, both of which are very non-trivial.
|
||||
|
||||
Folks who are determined to make this work anyway can try these manual steps to sign these `boot_otas` with your own key. Since the incremental OTAs are not being regenerated, the `boot` partition must be left unmodified when running `avbroot ota patch`.
|
||||
|
||||
1. Unpack `vendor.img` with avbroot and [afsr](https://github.com/chenxiaolong/afsr).
|
||||
|
||||
```bash
|
||||
avbroot avb unpack -i vendor.img
|
||||
afsr unpack -i raw.img
|
||||
```
|
||||
|
||||
2. Extract `payload.bin` from `boot_otas/boot_ota_16k.zip`.
|
||||
|
||||
3. Re-sign `payload.bin` with your OTA key.
|
||||
|
||||
```bash
|
||||
avbroot payload repack \
|
||||
-i payload.bin.orig \
|
||||
-o payload.bin \
|
||||
-k ota.key \
|
||||
--output-properties payload_properties.txt
|
||||
```
|
||||
|
||||
4. Create a new zip of `payload.bin` and `payload_properties.txt`. The files must be stored uncompressed (eg. with `zip -0`).
|
||||
|
||||
5. Repeat the procedure for `boot_otas/boot_ota_4k.zip`.
|
||||
|
||||
6. Repack `vendor.img` and sign it with your AVB key.
|
||||
|
||||
```bash
|
||||
afsr pack -o raw.img
|
||||
avbroot avb pack -o vendor.img -k avb.key --recompute-size
|
||||
```
|
||||
|
||||
7. Patch the (normal) OTA with:
|
||||
|
||||
```bash
|
||||
avbroot ota patch \
|
||||
--replace vendor <modified vendor> \
|
||||
<normal arguments...>
|
||||
```
|
||||
|
||||
## Building from source
|
||||
|
||||
Make sure the [Rust toolchain](https://www.rust-lang.org/) is installed. Then run:
|
||||
|
||||
+70
-8
@@ -43,7 +43,7 @@ avbroot модифицирует следующие образы:
|
||||
|
||||
## Использование
|
||||
|
||||
1. Убедитесь, что вы ознакомились и поняли указанные выше [предостережения.](#предостережения)
|
||||
1. Убедитесь, что вы ознакомились и поняли указанные выше [предостережения.](#предостережения)
|
||||
|
||||
2. Скачайте последнюю версию со страницы [релизов.](https://github.com/chenxiaolong/avbroot/releases) Чтобы сверить цифровую подпись, см. раздел [проверки цифровых подписей.](#проверка-цифровых-подписей)
|
||||
|
||||
@@ -107,7 +107,7 @@ avbroot модифицирует следующие образы:
|
||||
|
||||
Первые два компонента подписываются ключом AVB, а последние два – ключом OTA. Можно использовать один и тот же ключ, однако в следующих шагах описано, как сгенерировать два отдельных.
|
||||
|
||||
Если вы патчите OTA сразу для нескольких устройств, настоятельно рекомендуется генерировать уникальные ключи для каждого девайса – так вы защитите себя от случайной прошивки неподходящего OTA для другого телефона.
|
||||
Если вы патчите OTA сразу для нескольких устройств, настоятельно рекомендуется генерировать уникальные ключи для каждого девайса – так вы защитите себя от случайной прошивки неподходящего OTA для другого телефона.
|
||||
|
||||
1. Сгенерируйте ключи подписи для AVB и OTA.
|
||||
|
||||
@@ -119,7 +119,7 @@ avbroot модифицирует следующие образы:
|
||||
2. Преобразуйте публичную часть ключа подписи AVB в формат метаданных публичного ключа AVB. Именно этот формат используется в загрузчике устройства для установки пользовательского ключа.
|
||||
|
||||
```bash
|
||||
avbroot key extract-avb -k avb.key -o avb_pkmd.bin
|
||||
avbroot key encode-avb -k avb.key -o avb_pkmd.bin
|
||||
```
|
||||
|
||||
3. Сгенерируйте самоподписанный сертификат для ключа подписи OTA. Он используется режимом Recovery для проверки подписи OTA при сайдлоадинге обновления.
|
||||
@@ -295,6 +295,7 @@ Magisk версии 25211 и новее требует наличие разде
|
||||
--input /path/to/ota.zip \
|
||||
--directory . \
|
||||
--boot-only
|
||||
--partition <название раздела> # init_boot или boot, в зависимости от устройства
|
||||
```
|
||||
|
||||
2. Теперь нужно пропатчить загрузочный образ с помощью приложения Magisk. Это **ДОЛЖНО** быть сделано именно на целевом устройстве или устройстве той же модели! Имя раздела будет неверным и не подойдет, если пропатчить образ на устройстве иной модели.
|
||||
@@ -377,10 +378,21 @@ avbroot может подменить используемый загрузоч
|
||||
|
||||
Обратите внимание, что avbroot проверяет совместимость предварительно пропатченного образа с оригинальным. Например, если поля заголовка образа не совпадают, или вовсе указан иной, незагрузочный образ, то процесс патча будет прерван. Эти проверки, конечно, ничего не гарантируют, но должны предостеречь от случайного использования некорректного образа. Чтобы обойти базовые проверки безопасности, укажите аргумент `--ignore-prepatched-compat`. Если вы хотите убрать вообще все проверки (чего делать крайне не рекомендуется), укажите его дважды.
|
||||
|
||||
### Пропуск патчей для root-доступа
|
||||
### Пропуск патча для root-доступа
|
||||
|
||||
avbroot можно использовать для простого переподписания OTA, указав аргумент `--rootless` вместо `--magisk`/`--prepatched`. В таком случае пропатченный OTA не будет рутирован. Единственная модификация, которая будет применена – это замена сертификата проверки OTA, чтобы систему можно было обновлять с помощью будущих пропатченных OTA.
|
||||
|
||||
### Пропуск патчинга сертификата OTA
|
||||
|
||||
avbroot может пропускать изменение `otacerts.zip` с помощью аргументов `--skip-system-ota-cert` и `--skip-recovery-ota-cert`. **Не используйте их без веской причины.** (Например, если вы уже самостоятельно встроили сертификат OTA в загрузочный (boot) образ и передаете его программе через опции `--prepatched` или `--replace`.)
|
||||
В противном случае, на устройстве может не остаться возможности устанавливать дальнейшие обновления.
|
||||
|
||||
При использовании `--skip-system-ota-cert`, никаких изменений в образ `system` не вносится.
|
||||
|
||||
При использовании `--skip-recovery-ota-cert` совместно с `--rootless` и без указания `--dsu`, не вносится никаких изменений в загрузочные образы, кроме обеспечения их корректной подписи.
|
||||
|
||||
Если вы вручную добавили сертификат OTA в загрузочный (boot) образ, рекомендуем [предварительно проверить пропатченный OTA.](#проверка-ota)
|
||||
|
||||
### Подмена разделов
|
||||
|
||||
avbroot поддерживает подмену целых образов в OTA, даже тех, что не являются загрузочными (например, `vendor_dlkm`). Образ можно заменить, указав аргумент `--replace <имя раздела> /путь/к/образу.img`.
|
||||
@@ -435,17 +447,20 @@ Verified boot is disabled by vbmeta's header flags: 0x3
|
||||
|
||||
* Использовать незашифрованные приватные ключи. Крайне не рекомендуется.
|
||||
|
||||
### Извлечение всей OTA
|
||||
### Извлечение образов из OTA
|
||||
|
||||
Чтобы извлечь все образы, содержащиеся в `payload.bin`, используйте команду:
|
||||
Чтобы извлечь образы разделов, содержащихся в `payload.bin`, используйте команду:
|
||||
|
||||
```bash
|
||||
avbroot ota extract \
|
||||
--input /путь/к/ota.zip \
|
||||
--directory extracted \
|
||||
--all
|
||||
--directory extracted
|
||||
```
|
||||
|
||||
По умолчанию извлекаются только те образы, которые потенциально могут быть пропатчены с помощью avbroot. Чтобы извлечь все образы, используйте опцию `--all`. Для извлечения конкретных образов используйте опцию `--partition <название раздела>`, которую можно указать несколько раз.
|
||||
|
||||
Эта команда также поддерживает извлечение встроенного сертификата OTA и публичного ключа AVB с помощью опций `--cert-ota` и `--public-key-avb`. Чтобы извлечь только эти компоненты, укажите аргумент `--none`, чтобы пропустить извлечение образов разделов.
|
||||
|
||||
### Режим записи ZIP
|
||||
|
||||
По умолчанию, avbroot использует потоковую запись для вывода OTA во время патчинга. Это означает, что он вычисляет дайджест sha256 для цифровой подписи одновременно с записью файла. Такой режим приводит к тому, что в ZIP-файле появляются описатели данных, что является частью стандарта ZIP и работает на подавляющем большинстве устройств. Однако некоторые устройства могут иметь некорректно работающие парсеры ZIP-файлов и не смогут правильно прочитать ZIP-файлы OTA, содержащие описатели данных. Если это так, используйте опцию `--zip-mode seekable` при патчинге.
|
||||
@@ -474,6 +489,53 @@ avbroot поддерживает делегирование всех опера
|
||||
|
||||
Обратите внимание, что avbroot проверит подпись, возвращенную внешней программой, на соответствие с публичным ключом. Это гарантирует, что процесс патчинга завершится ошибкой, если был использован неправильный приватный ключ.
|
||||
|
||||
### Размер страницы 16 КБ в настройках для разработчиков
|
||||
|
||||
На современных устройствах с Android 16 и выше, в настройках для разработчиков может появиться опция переключения на ядро с размером страницы 16 КБ. Однако, эта функция не будет работать в системе, пропатченной с помощью avbroot, поскольку переключение данной настройки осуществляется путём установки инкрементальной OTA:
|
||||
|
||||
* `/vendor/boot_otas/boot_ota_16k.zip` — используется для переключения на ядро с размером страницы 16 КБ (в разделе `boot` уже должно быть прошито ядро с размером страницы 4K)
|
||||
* `/vendor/boot_otas/boot_ota_4k.zip` — используется для переключения на ядро с размером страницы 4 КБ (в разделе `boot` уже должно быть прошито ядро с размером страницы 16K)
|
||||
|
||||
Эти файлы (в `boot_otas`) невозможно прошить на системе, пропатченной avbroot, потому что `payload.bin` внутри них подписан ключом производителя. Кроме того, это неполноценные OTA-файлы: у них нет метаданных, характерных для OTA, а сам zip-файл не подписан. Это просто обычный архив, который содержит подписанный `payload.bin`.
|
||||
|
||||
Поддержка `boot_otas` не планируется. Это потребует реализации функционала для модификации ФС в инкрементальных OTA и их дальнейшей обработки, что сделать очень непросто.
|
||||
|
||||
Если вы всё же хотите завести эту функцию, можно попробовать вручную подписать файлы в `boot_otas` собственным ключом. Поскольку инкрементальные OTA не пересоздаются, раздел `boot` должен оставаться без изменений во время выполнения команды `avbroot ota patch`.
|
||||
|
||||
1. Распакуйте `vendor.img` с помощью avbroot и [afsr](https://github.com/chenxiaolong/afsr):
|
||||
```bash
|
||||
avbroot avb unpack -i vendor.img
|
||||
afsr unpack -i raw.img
|
||||
```
|
||||
|
||||
2. Извлеките `payload.bin` из `boot_otas/boot_ota_16k.zip`.
|
||||
|
||||
3. Переподпишите `payload.bin` вашим OTA-ключом:
|
||||
```bash
|
||||
avbroot payload repack \
|
||||
-i payload.bin.orig \
|
||||
-o payload.bin \
|
||||
-k ota.key \
|
||||
--output-properties payload_properties.txt
|
||||
```
|
||||
|
||||
4. Создайте новый zip, включающий `payload.bin` и `payload_properties.txt`. Файлы должны быть добавлены без сжатия (например, с помощью `zip -0`).
|
||||
|
||||
5. Повторите эту процедуру для `boot_otas/boot_ota_4k.zip`.
|
||||
|
||||
6. Соберите `vendor.img` обратно и подпишите его вашим AVB-ключом:
|
||||
```bash
|
||||
afsr pack -o raw.img
|
||||
avbroot avb pack -o vendor.img -k avb.key --recompute-size
|
||||
```
|
||||
|
||||
7. Пропатчите обычный OTA-архив с прошивкой, подменив `vendor` на модифицированный образ:
|
||||
```bash
|
||||
avbroot ota patch \
|
||||
--replace vendor <модифицированный vendor.img> \
|
||||
<дальше указываются аргументы, как при обычном патчинге>
|
||||
```
|
||||
|
||||
## Сборка из исходного кода
|
||||
|
||||
Убедитесь, что у вас установлен [набор инструментов Rust.](https://www.rust-lang.org/ru/) Затем выполните:
|
||||
|
||||
+16
-23
@@ -10,10 +10,15 @@ publish = false
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0.75"
|
||||
# We use aws-lc-rs instead of sha2 for sha256 digest computation of large files
|
||||
# because sha2 is significantly slower on older x86_64 CPUs without the SHA-NI
|
||||
# instructions. sha2 is still used for signing purposes.
|
||||
# https://github.com/RustCrypto/hashes/issues/327
|
||||
aws-lc-rs = { version = "1.0.0", default-features = false, features = ["aws-lc-sys"] }
|
||||
base64 = "0.22.1"
|
||||
bitflags = { version = "2.4.1", features = ["serde"] }
|
||||
bstr = "1.6.2"
|
||||
byteorder = "1.4.3"
|
||||
bzip2 = { version = "0.5.1", default-features = false, features = ["libbz2-rs-sys"] }
|
||||
cap-std = "3.0.0"
|
||||
cap-tempfile = "3.0.0"
|
||||
clap = { version = "4.4.1", features = ["derive"] }
|
||||
@@ -22,48 +27,36 @@ cms = { version = "0.2.2", features = ["std"] }
|
||||
const-oid = "0.9.5"
|
||||
crc32fast = "1.4.2"
|
||||
ctrlc = "3.4.0"
|
||||
dlv-list = "0.5.2"
|
||||
flate2 = "1.0.27"
|
||||
dlv-list = "0.6.0"
|
||||
flate2 = { version = "1.0.29", features = ["zlib-rs"] }
|
||||
gf256 = { version = "0.3.0", features = ["rs"] }
|
||||
hex = { version = "0.4.3", features = ["serde"] }
|
||||
liblzma = "0.3.0"
|
||||
lz4_flex = "0.11.1"
|
||||
memchr = "2.6.0"
|
||||
miniz_oxide = "0.8.0"
|
||||
num-bigint-dig = "0.8.4"
|
||||
num-traits = "0.2.16"
|
||||
passterm = "2.0.3"
|
||||
phf = { version = "0.11.2", features = ["macros"] }
|
||||
pkcs8 = { version = "0.10.2", features = ["encryption", "pem"] }
|
||||
prost = "0.13.1"
|
||||
# We can't upgrade to 0.9.0 until rsa updates its rand_core dependency.
|
||||
rand = "0.8.5"
|
||||
rayon = "1.7.0"
|
||||
regex = { version = "1.9.4", default-features = false, features = ["perf", "std"] }
|
||||
# We use ring instead of sha2 for sha256 digest computation of large files
|
||||
# because sha2 is significantly slower on older x86_64 CPUs without the SHA-NI
|
||||
# instructions. sha2 is still used for signing purposes.
|
||||
# https://github.com/RustCrypto/hashes/issues/327
|
||||
ring = "0.17.0"
|
||||
rsa = { version = "0.9.2", features = ["sha1", "sha2"] }
|
||||
serde = { version = "1.0.188", features = ["derive"] }
|
||||
sha1 = "0.10.5"
|
||||
sha2 = "0.10.7"
|
||||
tempfile = "3.8.0"
|
||||
thiserror = "1.0.47"
|
||||
thiserror = "2.0.3"
|
||||
toml_edit = { version = "0.22.9", features = ["serde"] }
|
||||
topological-sort = "0.2.2"
|
||||
tracing = "0.1.40"
|
||||
tracing-subscriber = "0.3.18"
|
||||
x509-cert = { version = "0.2.4", features = ["builder"] }
|
||||
zerocopy = "0.7.35"
|
||||
zerocopy-derive = "0.7.35"
|
||||
|
||||
# There are multiple upstream bugs that cause infinite loops in the Drop
|
||||
# implementation of write::BzDecoder. Unfortunately, the project is no longer
|
||||
# maintained, so we have to maintain our own fork with the necessary fixes.
|
||||
[dependencies.bzip2]
|
||||
git = "https://github.com/chenxiaolong/bzip2-rs"
|
||||
rev = "6e0f9836ec87b19261461b6cc1772e14aff8e851"
|
||||
zerocopy = { version = "0.8.10", features = ["std"] }
|
||||
zerocopy-derive = "0.8.5"
|
||||
|
||||
# https://github.com/zip-rs/zip/pull/383
|
||||
[dependencies.zip]
|
||||
@@ -77,7 +70,7 @@ libc = "0.2.158"
|
||||
rustix = { version = "0.38.9", default-features = false, features = ["process"] }
|
||||
|
||||
[build-dependencies]
|
||||
constcat = "0.5.0"
|
||||
constcat = "0.6.0"
|
||||
prost-build = "0.13.1"
|
||||
protox = "0.7.0"
|
||||
|
||||
@@ -85,7 +78,7 @@ protox = "0.7.0"
|
||||
assert_matches = "1.5.0"
|
||||
|
||||
[features]
|
||||
static = ["bzip2/static", "liblzma/static"]
|
||||
static = ["liblzma/static"]
|
||||
|
||||
[lints.rust]
|
||||
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(fuzzing)'] }
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
+2
-4
@@ -1,7 +1,5 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2023-2024 Andrew Gunnerson
|
||||
* SPDX-License-Identifier: GPL-3.0-only
|
||||
*/
|
||||
// SPDX-FileCopyrightText: 2023-2024 Andrew Gunnerson
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use std::{env, ffi::OsStr, fs, io, path::Path};
|
||||
|
||||
|
||||
@@ -316,6 +316,10 @@ message PartitionUpdate {
|
||||
// as a hint. If set to 0, libsnapshot should use alternative
|
||||
// methods for estimating size.
|
||||
optional uint64 estimate_cow_size = 19;
|
||||
|
||||
// Information about the cow used by Cow Writer to specify
|
||||
// number of cow operations to be written
|
||||
optional uint64 estimate_op_count_max = 20;
|
||||
}
|
||||
|
||||
message DynamicPartitionGroup {
|
||||
@@ -368,6 +372,10 @@ message DynamicPartitionMetadata {
|
||||
|
||||
// A collection of knobs to tune Virtual AB Compression
|
||||
optional VABCFeatureSet vabc_feature_set = 6;
|
||||
|
||||
// Max bytes to be compressed at once during ota. Options: 4k, 8k, 16k, 32k,
|
||||
// 64k, 128k
|
||||
optional uint64 compression_factor = 7;
|
||||
}
|
||||
|
||||
// Definition has been duplicated from
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2023-2024 Andrew Gunnerson
|
||||
* SPDX-License-Identifier: GPL-3.0-only
|
||||
*/
|
||||
// SPDX-FileCopyrightText: 2023-2024 Andrew Gunnerson
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use std::{
|
||||
fmt,
|
||||
@@ -32,10 +30,13 @@ pub enum Command {
|
||||
Payload(payload::PayloadCli),
|
||||
Sparse(sparse::SparseCli),
|
||||
/// (Deprecated: Use `avbroot ota patch` instead.)
|
||||
#[command(hide = true)]
|
||||
Patch(ota::PatchCli),
|
||||
/// (Deprecated: Use `avbroot ota extract` instead.)
|
||||
#[command(hide = true)]
|
||||
Extract(ota::ExtractCli),
|
||||
/// (Deprecated: Use `avbroot boot magisk-info` instead.)
|
||||
#[command(hide = true)]
|
||||
MagiskInfo(boot::MagiskInfoCli),
|
||||
}
|
||||
|
||||
|
||||
+132
-23
@@ -1,15 +1,12 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2023-2024 Andrew Gunnerson
|
||||
* SPDX-License-Identifier: GPL-3.0-only
|
||||
*/
|
||||
// SPDX-FileCopyrightText: 2023-2024 Andrew Gunnerson
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use std::{
|
||||
collections::{HashMap, HashSet},
|
||||
ffi::{OsStr, OsString},
|
||||
fs::{self, File},
|
||||
io::{self, BufReader, BufWriter, Seek, SeekFrom, Write},
|
||||
io::{self, BufReader, BufWriter, Cursor, Seek, SeekFrom, Write},
|
||||
path::{Path, PathBuf},
|
||||
str,
|
||||
sync::atomic::AtomicBool,
|
||||
};
|
||||
|
||||
@@ -30,7 +27,7 @@ use crate::{
|
||||
self, AlgorithmType, AppendedDescriptorMut, AppendedDescriptorRef, Descriptor, Footer,
|
||||
HashTreeDescriptor, Header, KernelCmdlineDescriptor,
|
||||
},
|
||||
stream::{self, PSeekFile, Reopen},
|
||||
stream::{self, check_cancel, PSeekFile, ReadFixedSizeExt, Reopen, ToWriter},
|
||||
util,
|
||||
};
|
||||
|
||||
@@ -342,7 +339,13 @@ fn sign_or_clear(info: &mut AvbInfo, orig_header: &Header, key_group: &KeyGroup)
|
||||
}
|
||||
|
||||
let originally_signed = !info.header.signature.is_empty();
|
||||
let mut sign_action = if originally_signed && &info.header != orig_header {
|
||||
let sign_action = if key_group.force {
|
||||
if key_group.key.is_some() {
|
||||
SignAction::Sign
|
||||
} else {
|
||||
SignAction::Clear
|
||||
}
|
||||
} else if originally_signed && &info.header != orig_header {
|
||||
SignAction::Sign
|
||||
} else {
|
||||
// If the original image was signed, we can preserve the existing
|
||||
@@ -351,14 +354,6 @@ fn sign_or_clear(info: &mut AvbInfo, orig_header: &Header, key_group: &KeyGroup)
|
||||
SignAction::None
|
||||
};
|
||||
|
||||
if key_group.force {
|
||||
sign_action = if key_group.key.is_some() {
|
||||
SignAction::Sign
|
||||
} else {
|
||||
SignAction::Clear
|
||||
};
|
||||
}
|
||||
|
||||
match sign_action {
|
||||
SignAction::None => {
|
||||
if originally_signed {
|
||||
@@ -400,7 +395,9 @@ fn sign_or_clear(info: &mut AvbInfo, orig_header: &Header, key_group: &KeyGroup)
|
||||
RsaSigningKey::Internal(private_key)
|
||||
};
|
||||
|
||||
info.header.set_algo_for_key(&signing_key)?;
|
||||
info.header
|
||||
.set_algo_for_key(&signing_key)
|
||||
.context("Failed to set signature algorithm")?;
|
||||
info.header
|
||||
.sign(&signing_key)
|
||||
.context("Failed to sign new AVB header")?;
|
||||
@@ -521,24 +518,21 @@ fn verify_and_repair(
|
||||
cancel_signal: &AtomicBool,
|
||||
) -> Result<()> {
|
||||
let _span = debug_span!("image", name = name.unwrap_or_default()).entered();
|
||||
let suffix = match name {
|
||||
Some(n) => format!(" for: {n}"),
|
||||
None => String::new(),
|
||||
};
|
||||
let suffix = name.map_or_else(String::new, |n| format!(" for: {n}"));
|
||||
|
||||
match descriptor {
|
||||
AppendedDescriptorRef::HashTree(d) => {
|
||||
info!("Verifying hash tree descriptor{suffix}");
|
||||
|
||||
match d.verify(&file, cancel_signal) {
|
||||
Err(e @ avb::Error::HashTree(_)) if repair => {
|
||||
Err(e @ avb::Error::HashTreeVerify(_)) if repair => {
|
||||
warn!("Failed to verify hash tree descriptor{suffix}: {e}");
|
||||
warn!("Attempting to repair using FEC data{suffix}");
|
||||
|
||||
d.repair(&file, &file, cancel_signal)
|
||||
.with_context(|| format!("Failed to repair data{suffix}"))?;
|
||||
|
||||
d.verify(&file, cancel_signal).map(|_| {
|
||||
d.verify(&file, cancel_signal).inspect(|()| {
|
||||
info!("Successfully repaired data{suffix}");
|
||||
})
|
||||
}
|
||||
@@ -602,6 +596,89 @@ pub fn verify_descriptors(
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn compute_digest_recursive(
|
||||
directory: &Dir,
|
||||
name: &str,
|
||||
context: &mut aws_lc_rs::digest::Context,
|
||||
max_depth: u8,
|
||||
seen: &mut HashSet<String>,
|
||||
cancel_signal: &AtomicBool,
|
||||
) -> Result<()> {
|
||||
if max_depth == 0 {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
check_cancel(cancel_signal)?;
|
||||
|
||||
seen.insert(name.to_owned());
|
||||
|
||||
ensure_name_is_safe(name)?;
|
||||
|
||||
let path = format!("{name}.img");
|
||||
let mut raw_reader = directory
|
||||
.open(&path)
|
||||
.map(BufReader::new)
|
||||
.with_context(|| format!("Failed to open for reading: {path:?}"))?;
|
||||
let (header, footer, _) = avb::load_image(&mut raw_reader)
|
||||
.with_context(|| format!("Failed to load vbmeta structures: {path:?}"))?;
|
||||
|
||||
// We don't have a good way to get the length of the header, so we serialize
|
||||
// what we just parsed and compare it to the raw file so ensure that the
|
||||
// round-tripped data is identical.
|
||||
let raw_header = {
|
||||
let mut writer = Cursor::new(Vec::new());
|
||||
header
|
||||
.to_writer(&mut writer)
|
||||
.with_context(|| format!("Failed to serialize header: {path:?}"))?;
|
||||
writer.into_inner()
|
||||
};
|
||||
|
||||
let header_offset = footer.map(|f| f.vbmeta_offset).unwrap_or_default();
|
||||
raw_reader
|
||||
.seek(SeekFrom::Start(header_offset))
|
||||
.with_context(|| format!("Failed to seek file: {path:?}"))?;
|
||||
|
||||
let raw_header_orig = raw_reader
|
||||
.read_vec_exact(raw_header.len())
|
||||
.with_context(|| format!("Failed to reread AVB header: {path:?}"))?;
|
||||
|
||||
if raw_header != raw_header_orig {
|
||||
bail!("Serialized header does not match original header: {path:?}");
|
||||
}
|
||||
|
||||
context.update(&raw_header);
|
||||
|
||||
for descriptor in &header.descriptors {
|
||||
if let avb::Descriptor::ChainPartition(d) = descriptor {
|
||||
compute_digest_recursive(
|
||||
directory,
|
||||
&d.partition_name,
|
||||
context,
|
||||
max_depth - 1,
|
||||
seen,
|
||||
cancel_signal,
|
||||
)?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Compute the vbmeta digest. This is defined as the digest of the header in
|
||||
/// the root vbmeta image, followed by the headers in the immediate chained
|
||||
/// partitions. This digest is not defined to be recursive, so headers of
|
||||
/// chained partitions more than one level deep are ignored.
|
||||
pub fn compute_digest(directory: &Dir, name: &str, cancel_signal: &AtomicBool) -> Result<[u8; 32]> {
|
||||
let mut seen = HashSet::<String>::new();
|
||||
let mut context = aws_lc_rs::digest::Context::new(&aws_lc_rs::digest::SHA256);
|
||||
|
||||
compute_digest_recursive(directory, name, &mut context, 2, &mut seen, cancel_signal)?;
|
||||
|
||||
let digest = context.finish();
|
||||
|
||||
Ok(digest.as_ref().try_into().unwrap())
|
||||
}
|
||||
|
||||
fn unpack_subcommand(cli: &UnpackCli, cancel_signal: &AtomicBool) -> Result<()> {
|
||||
let (info, mut reader) = read_avb_image(&cli.input)?;
|
||||
display_info(&cli.display, &info);
|
||||
@@ -738,6 +815,25 @@ fn verify_subcommand(cli: &VerifyCli, cancel_signal: &AtomicBool) -> Result<()>
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn digest_subcommand(cli: &DigestCli, cancel_signal: &AtomicBool) -> Result<()> {
|
||||
let authority = ambient_authority();
|
||||
let parent_path = util::parent_path(&cli.input);
|
||||
let directory = Dir::open_ambient_dir(parent_path, authority)
|
||||
.with_context(|| format!("Failed to open directory: {parent_path:?}"))?;
|
||||
let name = cli
|
||||
.input
|
||||
.file_stem()
|
||||
.with_context(|| format!("Path is not a file: {:?}", cli.input))?
|
||||
.to_str()
|
||||
.ok_or_else(|| anyhow!("Invalid UTF-8: {:?}", cli.input))?;
|
||||
|
||||
let digest = compute_digest(&directory, name, cancel_signal)?;
|
||||
|
||||
println!("{}", hex::encode(digest));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn avb_main(cli: &AvbCli, cancel_signal: &AtomicBool) -> Result<()> {
|
||||
match &cli.command {
|
||||
AvbCommand::Unpack(c) => unpack_subcommand(c, cancel_signal),
|
||||
@@ -745,6 +841,7 @@ pub fn avb_main(cli: &AvbCli, cancel_signal: &AtomicBool) -> Result<()> {
|
||||
AvbCommand::Repack(c) => repack_subcommand(c, cancel_signal),
|
||||
AvbCommand::Info(c) => info_subcommand(c),
|
||||
AvbCommand::Verify(c) => verify_subcommand(c, cancel_signal),
|
||||
AvbCommand::Digest(c) => digest_subcommand(c, cancel_signal),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -949,6 +1046,17 @@ struct VerifyCli {
|
||||
repair: bool,
|
||||
}
|
||||
|
||||
/// Compute the vbmeta digest.
|
||||
///
|
||||
/// This value is equal to what is reported by the ro.boot.vbmeta.digest
|
||||
/// property on a real device.
|
||||
#[derive(Debug, Parser)]
|
||||
struct DigestCli {
|
||||
/// Path to input AVB image.
|
||||
#[arg(short, long, value_name = "FILE", value_parser)]
|
||||
input: PathBuf,
|
||||
}
|
||||
|
||||
#[derive(Debug, Subcommand)]
|
||||
enum AvbCommand {
|
||||
Unpack(UnpackCli),
|
||||
@@ -957,6 +1065,7 @@ enum AvbCommand {
|
||||
#[command(alias = "dump")]
|
||||
Info(InfoCli),
|
||||
Verify(VerifyCli),
|
||||
Digest(DigestCli),
|
||||
}
|
||||
|
||||
/// Pack, unpack, and inspect AVB-protected images.
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2023 Andrew Gunnerson
|
||||
* SPDX-License-Identifier: GPL-3.0-only
|
||||
*/
|
||||
// SPDX-FileCopyrightText: 2023 Andrew Gunnerson
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use std::{
|
||||
fs::{self, File},
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2023 Andrew Gunnerson
|
||||
* SPDX-License-Identifier: GPL-3.0-only
|
||||
*/
|
||||
// SPDX-FileCopyrightText: 2023 Andrew Gunnerson
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use std::io;
|
||||
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2023 Andrew Gunnerson
|
||||
* SPDX-License-Identifier: GPL-3.0-only
|
||||
*/
|
||||
// SPDX-FileCopyrightText: 2023 Andrew Gunnerson
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use std::{
|
||||
fs::{self, File},
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2023 Andrew Gunnerson
|
||||
* SPDX-License-Identifier: GPL-3.0-only
|
||||
*/
|
||||
// SPDX-FileCopyrightText: 2023 Andrew Gunnerson
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use std::{
|
||||
fs::{File, OpenOptions},
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2023 Andrew Gunnerson
|
||||
* SPDX-License-Identifier: GPL-3.0-only
|
||||
*/
|
||||
// SPDX-FileCopyrightText: 2023 Andrew Gunnerson
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use std::{
|
||||
fs::{File, OpenOptions},
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2023 Andrew Gunnerson
|
||||
* SPDX-License-Identifier: GPL-3.0-only
|
||||
*/
|
||||
// SPDX-FileCopyrightText: 2023 Andrew Gunnerson
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use std::{
|
||||
ffi::OsString,
|
||||
@@ -48,7 +46,7 @@ pub fn key_main(cli: &KeyCli) -> Result<()> {
|
||||
crypto::write_pem_cert_file(&c.output, &cert)
|
||||
.with_context(|| format!("Failed to write certificate: {:?}", c.output))?;
|
||||
}
|
||||
KeyCommand::ExtractAvb(c) => {
|
||||
KeyCommand::ExtractAvb(c) | KeyCommand::EncodeAvb(c) => {
|
||||
let public_key = if let Some(p) = &c.input.key {
|
||||
let passphrase = get_passphrase_source(&c.passphrase, p);
|
||||
let private_key = crypto::read_pem_key_file(p, &passphrase)
|
||||
@@ -152,12 +150,12 @@ struct GenerateCertCli {
|
||||
validity: u64,
|
||||
}
|
||||
|
||||
/// Extract the AVB public key from a private key or certificate.
|
||||
/// Convert a key or certificate to an AVB-encoded public key.
|
||||
///
|
||||
/// The public key is stored in both the private key and the certificate. Either
|
||||
/// one can be used interchangeably.
|
||||
#[derive(Debug, Parser)]
|
||||
struct ExtractAvbCli {
|
||||
struct EncodeAvbCli {
|
||||
/// Path to output AVB public key.
|
||||
#[arg(short, long, value_name = "FILE", value_parser)]
|
||||
output: PathBuf,
|
||||
@@ -185,7 +183,10 @@ struct DecodeAvbCli {
|
||||
enum KeyCommand {
|
||||
GenerateKey(GenerateKeyCli),
|
||||
GenerateCert(GenerateCertCli),
|
||||
ExtractAvb(ExtractAvbCli),
|
||||
/// (Deprecated: Use `avbroot key encode-avb` instead.)
|
||||
#[command(hide = true)]
|
||||
ExtractAvb(EncodeAvbCli),
|
||||
EncodeAvb(EncodeAvbCli),
|
||||
DecodeAvb(DecodeAvbCli),
|
||||
}
|
||||
|
||||
|
||||
+12
-17
@@ -1,7 +1,5 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2024 Andrew Gunnerson
|
||||
* SPDX-License-Identifier: GPL-3.0-only
|
||||
*/
|
||||
// SPDX-FileCopyrightText: 2024 Andrew Gunnerson
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use std::{
|
||||
ffi::OsStr,
|
||||
@@ -131,22 +129,19 @@ fn split_extents(extents: &[Extent]) -> Vec<CopyExtent> {
|
||||
/// Use the CLI-specified slot or automatically select one if all slots are
|
||||
/// identical.
|
||||
fn get_slot_number(metadata: &Metadata, cli_slot: Option<u32>) -> Result<usize> {
|
||||
match cli_slot {
|
||||
Some(n) => {
|
||||
let n = n as usize;
|
||||
if n >= metadata.slots.len() {
|
||||
bail!("Slot out of range: {n}");
|
||||
}
|
||||
|
||||
Ok(n)
|
||||
if let Some(n) = cli_slot {
|
||||
let n = n as usize;
|
||||
if n >= metadata.slots.len() {
|
||||
bail!("Slot out of range: {n}");
|
||||
}
|
||||
None => {
|
||||
if metadata.slots.windows(2).any(|w| w[0] != w[1]) {
|
||||
bail!("A slot must be specified because they are not all identical");
|
||||
}
|
||||
|
||||
Ok(0)
|
||||
Ok(n)
|
||||
} else {
|
||||
if metadata.slots.windows(2).any(|w| w[0] != w[1]) {
|
||||
bail!("A slot must be specified because they are not all identical");
|
||||
}
|
||||
|
||||
Ok(0)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2023-2024 Andrew Gunnerson
|
||||
* SPDX-License-Identifier: GPL-3.0-only
|
||||
*/
|
||||
// SPDX-FileCopyrightText: 2023-2024 Andrew Gunnerson
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
pub mod args;
|
||||
pub mod avb;
|
||||
|
||||
+192
-51
@@ -1,7 +1,5 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2022-2024 Andrew Gunnerson
|
||||
* SPDX-License-Identifier: GPL-3.0-only
|
||||
*/
|
||||
// SPDX-FileCopyrightText: 2022-2025 Andrew Gunnerson
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use std::{
|
||||
borrow::Cow,
|
||||
@@ -10,7 +8,6 @@ use std::{
|
||||
fmt::Display,
|
||||
fs::{self, File},
|
||||
io::{self, BufReader, BufWriter, Read, Seek, SeekFrom, Write},
|
||||
mem,
|
||||
ops::Range,
|
||||
path::{Path, PathBuf},
|
||||
sync::{atomic::AtomicBool, Mutex},
|
||||
@@ -48,7 +45,7 @@ use crate::{
|
||||
},
|
||||
stream::{
|
||||
self, CountingWriter, FromReader, HashingWriter, PSeekFile, ReadSeekReopen, Reopen,
|
||||
SectionReader, ToWriter, WriteSeekReopen,
|
||||
SectionReader, SharedCursor, ToWriter, WriteSeekReopen,
|
||||
},
|
||||
util,
|
||||
};
|
||||
@@ -82,8 +79,9 @@ impl RequiredImages {
|
||||
let partitions = manifest
|
||||
.partitions
|
||||
.iter()
|
||||
.map(|p| p.partition_name.clone())
|
||||
.map(|p| &p.partition_name)
|
||||
.filter(|n| Self::is_boot(n) || Self::is_system(n) || Self::is_vbmeta(n))
|
||||
.cloned()
|
||||
.collect();
|
||||
|
||||
Self(partitions)
|
||||
@@ -195,7 +193,7 @@ fn open_input_files(
|
||||
fn patch_boot_images<'a, 'b: 'a>(
|
||||
required_images: &'b RequiredImages,
|
||||
input_files: &mut HashMap<String, InputFile>,
|
||||
boot_patchers: Vec<Box<dyn BootImagePatch + Sync>>,
|
||||
boot_patchers: &[Box<dyn BootImagePatch + Sync>],
|
||||
key_avb: &RsaSigningKey,
|
||||
cancel_signal: &AtomicBool,
|
||||
) -> Result<()> {
|
||||
@@ -203,7 +201,7 @@ fn patch_boot_images<'a, 'b: 'a>(
|
||||
let boot_partitions = required_images.iter_boot().collect::<Vec<_>>();
|
||||
|
||||
info!(
|
||||
"Patching boot images: {}",
|
||||
"Candidate boot images: {}",
|
||||
joined(sorted(boot_partitions.iter())),
|
||||
);
|
||||
|
||||
@@ -221,7 +219,7 @@ fn patch_boot_images<'a, 'b: 'a>(
|
||||
WriteSeekReopen::reopen_boxed(&input_file.file)
|
||||
},
|
||||
key_avb,
|
||||
&boot_patchers,
|
||||
boot_patchers,
|
||||
cancel_signal,
|
||||
)
|
||||
.with_context(|| {
|
||||
@@ -350,7 +348,7 @@ fn ensure_partitions_protected(
|
||||
/// determine the order to patch the vbmeta images so that it can be done in a
|
||||
/// single pass.
|
||||
fn get_vbmeta_patch_order(
|
||||
images: &mut HashMap<String, InputFile>,
|
||||
images: &HashMap<String, InputFile>,
|
||||
vbmeta_headers: &HashMap<String, Header>,
|
||||
) -> Result<Vec<(String, HashSet<String>)>> {
|
||||
let mut dep_graph = HashMap::<&str, HashSet<String>>::new();
|
||||
@@ -375,8 +373,9 @@ fn get_vbmeta_patch_order(
|
||||
.get_mut(vbmeta_name.as_str())
|
||||
.unwrap()
|
||||
.insert(partition_name.to_owned());
|
||||
missing.remove(partition_name);
|
||||
}
|
||||
|
||||
missing.remove(partition_name);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -594,8 +593,15 @@ fn update_vbmeta_headers(
|
||||
// Only sign and rewrite the image if we need to. Some vbmeta images may
|
||||
// have no dependencies and are only being processed to ensure that the
|
||||
// flags are set to a sane value.
|
||||
if parent_header != &orig_parent_header {
|
||||
parent_header.set_algo_for_key(key)?;
|
||||
//
|
||||
// The root vbmeta image is always signed because it is possible to
|
||||
// invoke avbroot is a way that no modifications are made (rootless +
|
||||
// skipping recovery otacerts.zip patch). We still want the result to be
|
||||
// bootable.
|
||||
if parent_header != &orig_parent_header || name == "vbmeta" {
|
||||
parent_header
|
||||
.set_algo_for_key(key)
|
||||
.with_context(|| format!("Failed to set signature algorithm: {name}"))?;
|
||||
parent_header
|
||||
.sign(key)
|
||||
.with_context(|| format!("Failed to sign vbmeta header for image: {name}"))?;
|
||||
@@ -722,7 +728,8 @@ fn patch_ota_payload(
|
||||
payload: &(dyn ReadSeekReopen + Sync),
|
||||
writer: impl Write,
|
||||
external_images: &HashMap<String, PathBuf>,
|
||||
boot_patchers: Vec<Box<dyn BootImagePatch + Sync>>,
|
||||
boot_patchers: &[Box<dyn BootImagePatch + Sync>],
|
||||
skip_system_ota_cert: bool,
|
||||
clear_vbmeta_flags: bool,
|
||||
key_avb: &RsaSigningKey,
|
||||
key_ota: &RsaSigningKey,
|
||||
@@ -777,24 +784,29 @@ fn patch_ota_payload(
|
||||
cancel_signal,
|
||||
)?;
|
||||
|
||||
// Main patching operation is done. Unmodified boot images no longer need to
|
||||
// be kept around.
|
||||
input_files
|
||||
.retain(|n, f| !(f.state == InputFileState::Extracted && RequiredImages::is_boot(n)));
|
||||
|
||||
let (system_target, system_ranges) = patch_system_image(
|
||||
&required_images,
|
||||
&mut input_files,
|
||||
cert_ota,
|
||||
key_avb,
|
||||
cancel_signal,
|
||||
)?;
|
||||
let system_result = if skip_system_ota_cert {
|
||||
None
|
||||
} else {
|
||||
Some(patch_system_image(
|
||||
&required_images,
|
||||
&mut input_files,
|
||||
cert_ota,
|
||||
key_avb,
|
||||
cancel_signal,
|
||||
)?)
|
||||
};
|
||||
|
||||
input_files
|
||||
.retain(|n, f| !(f.state == InputFileState::Extracted && RequiredImages::is_system(n)));
|
||||
|
||||
let mut vbmeta_headers = load_vbmeta_images(&mut input_files, &vbmeta_images)?;
|
||||
|
||||
ensure_partitions_protected(&required_images, &vbmeta_headers)?;
|
||||
|
||||
let mut vbmeta_order = get_vbmeta_patch_order(&mut input_files, &vbmeta_headers)?;
|
||||
let mut vbmeta_order = get_vbmeta_patch_order(&input_files, &vbmeta_headers)?;
|
||||
|
||||
info!(
|
||||
"Patching vbmeta images: {}",
|
||||
@@ -822,8 +834,12 @@ fn patch_ota_payload(
|
||||
&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) {
|
||||
Some(&system_ranges)
|
||||
if let Some((system_target, system_ranges)) = &system_result {
|
||||
if name == *system_target && !external_images.contains_key(&name) {
|
||||
Some(system_ranges)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
},
|
||||
@@ -914,7 +930,8 @@ fn patch_ota_zip(
|
||||
zip_reader: &mut ZipArchive<impl Read + Seek>,
|
||||
mut zip_writer: &mut ZipWriter<impl Write>,
|
||||
external_images: &HashMap<String, PathBuf>,
|
||||
mut boot_patchers: Vec<Box<dyn BootImagePatch + Sync>>,
|
||||
boot_patchers: &[Box<dyn BootImagePatch + Sync>],
|
||||
skip_system_ota_cert: bool,
|
||||
clear_vbmeta_flags: bool,
|
||||
zip_mode: ZipMode,
|
||||
key_avb: &RsaSigningKey,
|
||||
@@ -1014,7 +1031,7 @@ fn patch_ota_zip(
|
||||
// Use the user's certificate
|
||||
info!("Replacing zip entry: {path}");
|
||||
|
||||
crypto::write_pem_cert(&mut writer, cert_ota)
|
||||
crypto::write_pem_cert(Path::new(path), &mut writer, cert_ota)
|
||||
.with_context(|| format!("Failed to write entry: {path}"))?;
|
||||
}
|
||||
ota::PATH_PAYLOAD => {
|
||||
@@ -1036,8 +1053,8 @@ fn patch_ota_zip(
|
||||
&payload_reader,
|
||||
&mut writer,
|
||||
external_images,
|
||||
// There's only one payload in the OTA.
|
||||
mem::take(&mut boot_patchers),
|
||||
boot_patchers,
|
||||
skip_system_ota_cert,
|
||||
clear_vbmeta_flags,
|
||||
key_avb,
|
||||
key_ota,
|
||||
@@ -1151,7 +1168,7 @@ pub fn extract_payload(
|
||||
)
|
||||
.context("Failed to extract images from payload")?;
|
||||
|
||||
info!("Successfully extracted OTA");
|
||||
info!("Successfully extracted images from payload");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -1184,7 +1201,7 @@ fn verify_partition_hashes(
|
||||
|
||||
let mut writer = HashingWriter::new(
|
||||
io::sink(),
|
||||
ring::digest::Context::new(&ring::digest::SHA256),
|
||||
aws_lc_rs::digest::Context::new(&aws_lc_rs::digest::SHA256),
|
||||
);
|
||||
|
||||
stream::copy(file, &mut writer, cancel_signal)?;
|
||||
@@ -1306,7 +1323,15 @@ pub fn patch_subcommand(cli: &PatchCli, cancel_signal: &AtomicBool) -> Result<()
|
||||
assert!(cli.root.rootless);
|
||||
};
|
||||
|
||||
boot_patchers.push(Box::new(OtaCertPatcher::new(cert_ota.clone())));
|
||||
if cli.skip_system_ota_cert {
|
||||
warn!("Not inserting OTA cert into system image; sideloading further updates may fail");
|
||||
}
|
||||
|
||||
if cli.skip_recovery_ota_cert {
|
||||
warn!("Not inserting OTA cert into recovery image; sideloading further updates may fail");
|
||||
} else {
|
||||
boot_patchers.push(Box::new(OtaCertPatcher::new(cert_ota.clone())));
|
||||
}
|
||||
|
||||
if cli.dsu {
|
||||
boot_patchers.push(Box::new(DsuPubKeyPatcher::new(key_avb.to_public_key())));
|
||||
@@ -1343,7 +1368,8 @@ pub fn patch_subcommand(cli: &PatchCli, cancel_signal: &AtomicBool) -> Result<()
|
||||
&mut zip_reader,
|
||||
&mut zip_writer,
|
||||
&external_images,
|
||||
boot_patchers,
|
||||
&boot_patchers,
|
||||
cli.skip_system_ota_cert,
|
||||
cli.clear_vbmeta_flags,
|
||||
cli.zip_mode,
|
||||
&key_avb,
|
||||
@@ -1407,7 +1433,7 @@ pub fn extract_subcommand(cli: &ExtractCli, cancel_signal: &AtomicBool) -> Resul
|
||||
warn!("Ignoring --boot-partition: deprecated and no longer needed");
|
||||
}
|
||||
|
||||
let raw_reader = File::open(&cli.input)
|
||||
let mut raw_reader = File::open(&cli.input)
|
||||
.map(PSeekFile::new)
|
||||
.with_context(|| format!("Failed to open for reading: {:?}", cli.input))?;
|
||||
let mut zip = ZipArchive::new(BufReader::new(raw_reader.reopen()?))
|
||||
@@ -1435,7 +1461,7 @@ pub fn extract_subcommand(cli: &ExtractCli, cancel_signal: &AtomicBool) -> Resul
|
||||
|
||||
let mut unique_images = BTreeSet::new();
|
||||
|
||||
if cli.all {
|
||||
if cli.extract.all {
|
||||
unique_images.extend(
|
||||
header
|
||||
.manifest
|
||||
@@ -1444,16 +1470,68 @@ pub fn extract_subcommand(cli: &ExtractCli, cancel_signal: &AtomicBool) -> Resul
|
||||
.map(|p| &p.partition_name)
|
||||
.cloned(),
|
||||
);
|
||||
} else {
|
||||
} else if !cli.extract.partition.is_empty() {
|
||||
// We check this later too, but also do it here so we don't create a
|
||||
// bunch of empty files before failing.
|
||||
let valid_images = header
|
||||
.manifest
|
||||
.partitions
|
||||
.iter()
|
||||
.map(|p| &p.partition_name)
|
||||
.collect::<BTreeSet<_>>();
|
||||
let missing_images = cli
|
||||
.extract
|
||||
.partition
|
||||
.iter()
|
||||
.filter(|p| !valid_images.contains(p))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
if !missing_images.is_empty() {
|
||||
bail!("Invalid partitions: {}", joined(missing_images));
|
||||
}
|
||||
|
||||
unique_images.extend(cli.extract.partition.iter().cloned());
|
||||
} else if !cli.extract.none {
|
||||
let images = RequiredImages::new(&header.manifest);
|
||||
|
||||
if cli.boot_only {
|
||||
if cli.extract.boot_only {
|
||||
unique_images.extend(images.iter_boot().map(|n| n.to_owned()));
|
||||
} else {
|
||||
unique_images.extend(images.iter().map(|n| n.to_owned()));
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(path) = &cli.cert_ota {
|
||||
info!("Extracting embedded OTA certificate from zip signature");
|
||||
|
||||
let ota_sig = ota::parse_ota_sig(&mut raw_reader)?;
|
||||
let embedded_cert = ota_sig.embedded_cert()?;
|
||||
|
||||
crypto::write_pem_cert_file(path, embedded_cert)
|
||||
.with_context(|| format!("Failed to write OTA certificate: {path:?}"))?;
|
||||
}
|
||||
|
||||
if let Some(path) = &cli.public_key_avb {
|
||||
info!("Extracting AVB public key from vbmeta image");
|
||||
|
||||
let mut data = SharedCursor::new();
|
||||
|
||||
payload::extract_image(&payload_reader, &data, &header, "vbmeta", cancel_signal)
|
||||
.context("Failed to extract vbmeta image")?;
|
||||
|
||||
data.rewind()?;
|
||||
|
||||
let (header, _, _) = avb::load_image(data).context("Failed to parse vbmeta image")?;
|
||||
|
||||
fs::write(path, header.public_key)
|
||||
.with_context(|| format!("Failed to write AVB public key: {path:?}"))?;
|
||||
}
|
||||
|
||||
if unique_images.is_empty() {
|
||||
info!("No partition images to extract");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let authority = ambient_authority();
|
||||
Dir::create_ambient_dir_all(&cli.directory, authority)
|
||||
.with_context(|| format!("Failed to create directory: {:?}", cli.directory))?;
|
||||
@@ -1629,7 +1707,7 @@ pub fn verify_subcommand(cli: &VerifyCli, cancel_signal: &AtomicBool) -> Result<
|
||||
);
|
||||
} else if let Some(p) = &cli.cert_ota {
|
||||
let verify_cert = crypto::read_pem_cert_file(p)
|
||||
.with_context(|| format!("Failed to load certificate: {:?}", p))?;
|
||||
.with_context(|| format!("Failed to load certificate: {p:?}"))?;
|
||||
|
||||
if embedded_cert != verify_cert {
|
||||
bail!("OTA has a valid signature, but was not signed with: {p:?}");
|
||||
@@ -1686,9 +1764,11 @@ pub fn verify_subcommand(cli: &VerifyCli, cancel_signal: &AtomicBool) -> Result<
|
||||
|
||||
verify_partition_hashes(&temp_dir, &header, &unique_images, cancel_signal)?;
|
||||
|
||||
info!("Checking ramdisk's otacerts.zip");
|
||||
if cli.skip_recovery_ota_cert {
|
||||
warn!("Not verifying recovery ramdisk's otacerts.zip");
|
||||
} else {
|
||||
info!("Checking recovery ramdisk's otacerts.zip");
|
||||
|
||||
{
|
||||
let required_images = RequiredImages::new(&header.manifest);
|
||||
let boot_images =
|
||||
boot::load_boot_images(&required_images.iter_boot().collect::<Vec<_>>(), |name| {
|
||||
@@ -1782,7 +1862,7 @@ pub struct RootGroup {
|
||||
pub rootless: bool,
|
||||
}
|
||||
|
||||
/// Patch a full OTA zip.
|
||||
/// Patch a full OTA.
|
||||
#[derive(Debug, Parser)]
|
||||
pub struct PatchCli {
|
||||
/// Patch to original OTA zip.
|
||||
@@ -1924,6 +2004,25 @@ pub struct PatchCli {
|
||||
)]
|
||||
pub ignore_prepatched_compat: u8,
|
||||
|
||||
/// Skip adding OTA certificate to system image.
|
||||
///
|
||||
/// DO NOT USE THIS unless you've manually added the certificate to the
|
||||
/// system image already. Otherwise, installing further updates via a custom
|
||||
/// OTA updater app while booted into Android will not be possible.
|
||||
#[arg(long, help_heading = HEADING_OTHER)]
|
||||
pub skip_system_ota_cert: bool,
|
||||
|
||||
/// Skip adding OTA certificate to recovery image.
|
||||
///
|
||||
/// DO NOT USE THIS unless you've manually added the certificate to the
|
||||
/// recovery image already. Otherwise, sideloading further updates while
|
||||
/// booted into recovery mode will not be possible.
|
||||
///
|
||||
/// When this option is used with --rootless, the boot images in the OTA
|
||||
/// will not be modified.
|
||||
#[arg(long, help_heading = HEADING_OTHER)]
|
||||
pub skip_recovery_ota_cert: bool,
|
||||
|
||||
/// Add AVB public key to trusted keys for DSU.
|
||||
#[arg(long, help_heading = HEADING_OTHER)]
|
||||
pub dsu: bool,
|
||||
@@ -1960,7 +2059,33 @@ pub struct PatchCli {
|
||||
pub boot_partition: Option<String>,
|
||||
}
|
||||
|
||||
/// Extract partition images from an OTA zip's payload.
|
||||
#[derive(Debug, Args)]
|
||||
#[group(multiple = false)]
|
||||
pub struct ExtractGroup {
|
||||
/// Extract all images from the payload.
|
||||
///
|
||||
/// By default, only images that could potentially be patched by avbroot are
|
||||
/// extracted.
|
||||
#[arg(short, long)]
|
||||
pub all: bool,
|
||||
|
||||
/// Don't extract anything from the payload.
|
||||
///
|
||||
/// This is useful for using --cert-ota and --public-key-avb without
|
||||
/// extracting any partition images.
|
||||
#[arg(short, long)]
|
||||
pub none: bool,
|
||||
|
||||
/// (Deprecated: Specify an exact partition name instead.)
|
||||
#[arg(long, hide = true)]
|
||||
pub boot_only: bool,
|
||||
|
||||
/// Extract specific images from the payload.
|
||||
#[arg(short, long)]
|
||||
pub partition: Vec<String>,
|
||||
}
|
||||
|
||||
/// Extract a full OTA.
|
||||
#[derive(Debug, Parser)]
|
||||
pub struct ExtractCli {
|
||||
/// Path to OTA zip.
|
||||
@@ -1971,13 +2096,8 @@ pub struct ExtractCli {
|
||||
#[arg(short, long, value_parser, default_value = ".")]
|
||||
pub directory: PathBuf,
|
||||
|
||||
/// Extract all images from the payload.
|
||||
#[arg(short, long, group = "extract")]
|
||||
pub all: bool,
|
||||
|
||||
/// Extract only the boot image.
|
||||
#[arg(long, group = "extract")]
|
||||
pub boot_only: bool,
|
||||
#[command(flatten)]
|
||||
pub extract: ExtractGroup,
|
||||
|
||||
/// (Deprecated: no longer needed)
|
||||
#[arg(long, value_name = "PARTITION", hide = true)]
|
||||
@@ -1986,6 +2106,18 @@ pub struct ExtractCli {
|
||||
/// Generate fastboot info files.
|
||||
#[arg(long)]
|
||||
pub fastboot: bool,
|
||||
|
||||
/// Extract OTA certificate to file.
|
||||
///
|
||||
/// This is not extracted by default.
|
||||
#[arg(long, value_name = "FILE", value_parser)]
|
||||
pub cert_ota: Option<PathBuf>,
|
||||
|
||||
/// Extract AVB public key to file.
|
||||
///
|
||||
/// This is not extracted by default.
|
||||
#[arg(long, value_name = "FILE", value_parser)]
|
||||
pub public_key_avb: Option<PathBuf>,
|
||||
}
|
||||
|
||||
/// Verify signatures of an OTA.
|
||||
@@ -2010,6 +2142,15 @@ pub struct VerifyCli {
|
||||
/// valid, not that they are trusted.
|
||||
#[arg(long, value_name = "FILE", value_parser)]
|
||||
pub public_key_avb: Option<PathBuf>,
|
||||
|
||||
/// Skip verifying OTA certificate in recovery image.
|
||||
///
|
||||
/// This should not be used unless the OTA uses a special boot image format
|
||||
/// that avbroot cannot parse. This certificate check ensures that the OTA
|
||||
/// is configured properly to allow sideloading further OTAs signed by the
|
||||
/// same key.
|
||||
#[arg(long, help_heading = HEADING_OTHER)]
|
||||
pub skip_recovery_ota_cert: bool,
|
||||
}
|
||||
|
||||
#[allow(clippy::large_enum_variant)]
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2024 Andrew Gunnerson
|
||||
* SPDX-License-Identifier: GPL-3.0-only
|
||||
*/
|
||||
// SPDX-FileCopyrightText: 2024 Andrew Gunnerson
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2024 Andrew Gunnerson
|
||||
* SPDX-License-Identifier: GPL-3.0-only
|
||||
*/
|
||||
// SPDX-FileCopyrightText: 2024 Andrew Gunnerson
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use std::{
|
||||
fmt,
|
||||
@@ -15,7 +13,7 @@ use std::{
|
||||
use anyhow::{anyhow, bail, Context, Result};
|
||||
use clap::{Parser, Subcommand};
|
||||
use crc32fast::Hasher;
|
||||
use zerocopy::{little_endian, AsBytes};
|
||||
use zerocopy::{little_endian, IntoBytes};
|
||||
|
||||
use crate::{
|
||||
format::{
|
||||
@@ -30,7 +28,7 @@ use crate::{
|
||||
|
||||
struct CompactView<'a, T>(&'a [T]);
|
||||
|
||||
impl<'a, T: fmt::Debug> fmt::Debug for CompactView<'a, T> {
|
||||
impl<T: fmt::Debug> fmt::Debug for CompactView<'_, T> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
let mut list = f.debug_list();
|
||||
|
||||
@@ -112,7 +110,7 @@ fn split_chunks(chunks: &[Chunk], block_size: u32) -> Vec<Chunk> {
|
||||
#[cfg(any(target_os = "linux", target_os = "android"))]
|
||||
fn find_allocated_regions(
|
||||
path: &Path,
|
||||
reader: &mut File,
|
||||
reader: &File,
|
||||
cancel_signal: &AtomicBool,
|
||||
) -> Result<Vec<Range<u64>>> {
|
||||
use rustix::{fs::SeekFrom, io::Errno};
|
||||
@@ -124,13 +122,13 @@ fn find_allocated_regions(
|
||||
loop {
|
||||
stream::check_cancel(cancel_signal)?;
|
||||
|
||||
start = match rustix::fs::seek(&*reader, SeekFrom::Data(end as i64)) {
|
||||
start = match rustix::fs::seek(reader, SeekFrom::Data(end as i64)) {
|
||||
Ok(offset) => offset,
|
||||
Err(e) if e == Errno::NXIO => break,
|
||||
Err(e) => return Err(e).with_context(|| format!("Failed to seek to data: {path:?}")),
|
||||
};
|
||||
|
||||
end = rustix::fs::seek(&*reader, SeekFrom::Hole(start as i64))
|
||||
end = rustix::fs::seek(reader, SeekFrom::Hole(start as i64))
|
||||
.with_context(|| format!("Failed to seek to hole: {path:?}"))?;
|
||||
|
||||
result.push(start..end);
|
||||
@@ -377,12 +375,13 @@ fn pack_subcommand(
|
||||
} else {
|
||||
#[cfg(any(target_os = "linux", target_os = "android"))]
|
||||
{
|
||||
let regions = find_allocated_regions(&cli.input, &mut reader, cancel_signal)?;
|
||||
let regions = find_allocated_regions(&cli.input, &reader, cancel_signal)?;
|
||||
|
||||
(regions, false)
|
||||
}
|
||||
#[cfg(not(any(target_os = "linux", target_os = "android")))]
|
||||
{
|
||||
#[allow(clippy::single_range_in_vec_init)]
|
||||
(vec![0..file_size], true)
|
||||
}
|
||||
};
|
||||
|
||||
+155
-104
@@ -1,13 +1,11 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2023-2024 Andrew Gunnerson
|
||||
* SPDX-License-Identifier: GPL-3.0-only
|
||||
*/
|
||||
// SPDX-FileCopyrightText: 2023-2025 Andrew Gunnerson
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use std::{
|
||||
env::{self, VarError},
|
||||
ffi::{OsStr, OsString},
|
||||
fs::{self, File, OpenOptions},
|
||||
io::{self, BufReader, BufWriter, Read, Write},
|
||||
io::{self, Read, Write},
|
||||
path::{Path, PathBuf},
|
||||
process::{Command, ExitStatus, Stdio},
|
||||
time::Duration,
|
||||
@@ -44,20 +42,22 @@ use x509_cert::{
|
||||
Certificate,
|
||||
};
|
||||
|
||||
use crate::util::DebugString;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum Error {
|
||||
#[error("Signature algorithm not supported: {0:?}")]
|
||||
UnsupportedAlgorithm(SignatureAlgorithm),
|
||||
#[error("RSA key size ({}) not supported", .0 * 8)]
|
||||
UnsupportedKey(usize),
|
||||
UnsupportedKeySize(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("Failed to run command: {0:?}")]
|
||||
CommandSpawn(DebugString, #[source] io::Error),
|
||||
#[error("Command failed with status: {1}: {0:?}")]
|
||||
CommandExecution(DebugString, ExitStatus),
|
||||
#[error("Signature from signing helper does not match public key: {0:?}")]
|
||||
SigningHelperBadSignature(PathBuf),
|
||||
#[error("Passphrase prompt requires an interactive terminal")]
|
||||
@@ -70,28 +70,40 @@ pub enum Error {
|
||||
InvalidEnvVar(OsString, #[source] VarError),
|
||||
#[error("PEM has start tag, but no end tag")]
|
||||
PemNoEndTag,
|
||||
#[error("Failed to load encrypted private key")]
|
||||
#[error("Failed to load encrypted RSA private key")]
|
||||
LoadKeyEncrypted(#[source] pkcs8::Error),
|
||||
#[error("Failed to load unencrypted private key")]
|
||||
#[error("Failed to load unencrypted RSA private key")]
|
||||
LoadKeyUnencrypted(#[source] pkcs8::Error),
|
||||
#[error("Failed to save encrypted private key")]
|
||||
#[error("Failed to save encrypted RSA private key")]
|
||||
SaveKeyEncrypted(#[source] pkcs8::Error),
|
||||
#[error("Failed to save unencrypted private key")]
|
||||
#[error("Failed to save unencrypted RSA private key")]
|
||||
SaveKeyUnencrypted(#[source] pkcs8::Error),
|
||||
#[error("Failed to load RSA public key")]
|
||||
LoadPubKey(#[source] pkcs8::spki::Error),
|
||||
#[error("Failed to save RSA public key")]
|
||||
SavePubKey(#[source] pkcs8::spki::Error),
|
||||
#[error("Failed to load X509 certificate")]
|
||||
LoadCert(#[source] x509_cert::der::Error),
|
||||
#[error("Failed to save X509 certificate")]
|
||||
SaveCert(#[source] x509_cert::der::Error),
|
||||
#[error("Failed to generate RSA key")]
|
||||
RsaGenerate(#[source] Box<rsa::Error>),
|
||||
#[error("Failed to RSA sign digest")]
|
||||
RsaSign(#[source] rsa::Error),
|
||||
RsaSign(#[source] Box<rsa::Error>),
|
||||
#[error("Failed to RSA verify signature")]
|
||||
RsaVerify(#[source] rsa::Error),
|
||||
#[error("X509 error")]
|
||||
X509(#[from] x509_cert::builder::Error),
|
||||
#[error("SPKI error")]
|
||||
Spki(#[from] pkcs8::spki::Error),
|
||||
#[error("DER error")]
|
||||
Der(#[from] x509_cert::der::Error),
|
||||
#[error("RSA error")]
|
||||
Rsa(#[from] rsa::Error),
|
||||
#[error("I/O error")]
|
||||
Io(#[from] io::Error),
|
||||
RsaVerify(#[source] Box<rsa::Error>),
|
||||
#[error("Failed to generate X509 certificate")]
|
||||
CertGenerate(#[source] x509_cert::builder::Error),
|
||||
#[error("Invalid parameters for X509 certificate generation")]
|
||||
CertParams(#[source] x509_cert::der::Error),
|
||||
#[error("Failed to CMS sign digest")]
|
||||
CmsSign(#[source] x509_cert::der::Error),
|
||||
#[error("Failed to parse CMS signature")]
|
||||
CmsParse(#[source] x509_cert::der::Error),
|
||||
#[error("Failed to read file: {0:?}")]
|
||||
ReadFile(PathBuf, #[source] io::Error),
|
||||
#[error("Failed to write file: {0:?}")]
|
||||
WriteFile(PathBuf, #[source] io::Error),
|
||||
}
|
||||
|
||||
type Result<T> = std::result::Result<T, Error>;
|
||||
@@ -177,8 +189,9 @@ impl PassphraseSource {
|
||||
first
|
||||
}
|
||||
Self::EnvVar(v) => env::var(v).map_err(|e| Error::InvalidEnvVar(v.clone(), e))?,
|
||||
Self::File(p) => fs::read_to_string(p)?
|
||||
.trim_end_matches(&['\r', '\n'])
|
||||
Self::File(p) => fs::read_to_string(p)
|
||||
.map_err(|e| Error::ReadFile(p.clone(), e))?
|
||||
.trim_end_matches(['\r', '\n'])
|
||||
.to_owned(),
|
||||
};
|
||||
|
||||
@@ -189,7 +202,7 @@ 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));
|
||||
return Err(Error::UnsupportedKeySize(size));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@@ -237,8 +250,8 @@ impl RsaSigningKey {
|
||||
/// 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(),
|
||||
Self::Internal(key) => key.to_public_key(),
|
||||
Self::External { public_key, .. } => public_key.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -258,7 +271,9 @@ impl RsaSigningKey {
|
||||
};
|
||||
|
||||
match self {
|
||||
Self::Internal(key) => key.sign(scheme, digest).map_err(Error::RsaSign),
|
||||
Self::Internal(key) => key
|
||||
.sign(scheme, digest)
|
||||
.map_err(|e| Error::RsaSign(Box::new(e))),
|
||||
Self::External {
|
||||
program,
|
||||
public_key,
|
||||
@@ -294,7 +309,7 @@ impl RsaSigningKey {
|
||||
|
||||
let mut child = command
|
||||
.spawn()
|
||||
.map_err(|e| Error::CommandSpawnFailed(format!("{command:?}"), e))?;
|
||||
.map_err(|e| Error::CommandSpawn(DebugString::new(&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
|
||||
@@ -306,14 +321,22 @@ impl RsaSigningKey {
|
||||
// * 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 padded_digest = pkcs1v15_sign_pad(&scheme.prefix, digest, public_key.size())
|
||||
.map_err(|e| Error::RsaSign(Box::new(e)))?;
|
||||
child
|
||||
.stdin
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.write_all(&padded_digest)
|
||||
.map_err(|e| Error::WriteFile("<signing helper stdin>".into(), e))?;
|
||||
|
||||
let child = child.wait_with_output()?;
|
||||
let child = child
|
||||
.wait_with_output()
|
||||
.map_err(|e| Error::CommandSpawn(DebugString::new(&command), e))?;
|
||||
|
||||
if !child.status.success() {
|
||||
return Err(Error::CommandExecutionFailed(
|
||||
format!("{command:?}"),
|
||||
return Err(Error::CommandExecution(
|
||||
DebugString::new(&command),
|
||||
child.status,
|
||||
));
|
||||
} else if child.stdout.len() != self.size() {
|
||||
@@ -357,7 +380,7 @@ impl RsaPublicKeyExt for RsaPublicKey {
|
||||
};
|
||||
|
||||
self.verify(scheme, digest, signature)
|
||||
.map_err(Error::RsaVerify)
|
||||
.map_err(|e| Error::RsaVerify(Box::new(e)))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -366,7 +389,7 @@ pub fn generate_rsa_key_pair() -> Result<RsaPrivateKey> {
|
||||
let mut rng = rand::thread_rng();
|
||||
|
||||
// avbroot supports 4096-bit keys only.
|
||||
let key = RsaPrivateKey::new(&mut rng, 4096)?;
|
||||
let key = RsaPrivateKey::new(&mut rng, 4096).map_err(|e| Error::RsaGenerate(Box::new(e)))?;
|
||||
|
||||
Ok(key)
|
||||
}
|
||||
@@ -378,20 +401,27 @@ pub fn generate_cert(
|
||||
validity: Duration,
|
||||
subject: &str,
|
||||
) -> Result<Certificate> {
|
||||
let public_key_der = key.to_public_key().to_public_key_der()?;
|
||||
let public_key_der = key
|
||||
.to_public_key()
|
||||
.to_public_key_der()
|
||||
.map_err(Error::SavePubKey)?;
|
||||
let signing_key = SigningKey::<Sha256>::new(key.clone());
|
||||
|
||||
let builder = CertificateBuilder::new(
|
||||
Profile::Root,
|
||||
SerialNumber::from(serial),
|
||||
Validity::from_now(validity)?,
|
||||
subject.parse()?,
|
||||
SubjectPublicKeyInfoOwned::from_der(public_key_der.as_bytes())?,
|
||||
Validity::from_now(validity).map_err(Error::CertParams)?,
|
||||
subject.parse().map_err(Error::CertParams)?,
|
||||
SubjectPublicKeyInfoOwned::from_der(public_key_der.as_bytes())
|
||||
.map_err(Error::CertParams)?,
|
||||
&signing_key,
|
||||
)?;
|
||||
)
|
||||
.map_err(Error::CertGenerate)?;
|
||||
|
||||
let mut rng = rand::thread_rng();
|
||||
let cert = builder.build_with_rng(&mut rng)?;
|
||||
let cert = builder
|
||||
.build_with_rng(&mut rng)
|
||||
.map_err(Error::CertGenerate)?;
|
||||
|
||||
Ok(cert)
|
||||
}
|
||||
@@ -444,80 +474,92 @@ fn reformat_pem(data: &[u8]) -> Result<Vec<u8>> {
|
||||
}
|
||||
|
||||
/// Read PEM-encoded certificate from a reader.
|
||||
pub fn read_pem_cert(mut reader: impl Read) -> Result<Certificate> {
|
||||
pub fn read_pem_cert(path: &Path, mut reader: impl Read) -> Result<Certificate> {
|
||||
let mut data = vec![];
|
||||
reader.read_to_end(&mut data)?;
|
||||
reader
|
||||
.read_to_end(&mut data)
|
||||
.map_err(|e| Error::ReadFile(path.to_owned(), e))?;
|
||||
|
||||
let data = reformat_pem(&data)?;
|
||||
let certificate = Certificate::from_pem(data)?;
|
||||
let certificate = Certificate::from_pem(data).map_err(Error::LoadCert)?;
|
||||
|
||||
Ok(certificate)
|
||||
}
|
||||
|
||||
/// Write PEM-encoded certificate to a writer.
|
||||
pub fn write_pem_cert(mut writer: impl Write, cert: &Certificate) -> Result<()> {
|
||||
let data = cert.to_pem(LineEnding::LF)?;
|
||||
pub fn write_pem_cert(path: &Path, mut writer: impl Write, cert: &Certificate) -> Result<()> {
|
||||
let data = cert.to_pem(LineEnding::LF).map_err(Error::SaveCert)?;
|
||||
|
||||
writer.write_all(data.as_bytes())?;
|
||||
writer
|
||||
.write_all(data.as_bytes())
|
||||
.map_err(|e| Error::WriteFile(path.to_owned(), e))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Read PEM-encoded certificate from a file.
|
||||
pub fn read_pem_cert_file(path: &Path) -> Result<Certificate> {
|
||||
let file = File::open(path)?;
|
||||
let reader = BufReader::new(file);
|
||||
let reader = File::open(path).map_err(|e| Error::ReadFile(path.to_owned(), e))?;
|
||||
|
||||
read_pem_cert(reader)
|
||||
read_pem_cert(path, reader)
|
||||
}
|
||||
|
||||
/// Write PEM-encoded certificate to a file.
|
||||
pub fn write_pem_cert_file(path: &Path, cert: &Certificate) -> Result<()> {
|
||||
let file = File::create(path)?;
|
||||
let writer = BufWriter::new(file);
|
||||
let writer = File::create(path).map_err(|e| Error::WriteFile(path.to_owned(), e))?;
|
||||
|
||||
write_pem_cert(writer, cert)
|
||||
write_pem_cert(path, writer, cert)
|
||||
}
|
||||
|
||||
/// Read PEM-encoded PKCS8 public key from a reader.
|
||||
pub fn read_pem_public_key(mut reader: impl Read) -> Result<RsaPublicKey> {
|
||||
pub fn read_pem_public_key(path: &Path, mut reader: impl Read) -> Result<RsaPublicKey> {
|
||||
let mut data = String::new();
|
||||
reader.read_to_string(&mut data)?;
|
||||
reader
|
||||
.read_to_string(&mut data)
|
||||
.map_err(|e| Error::ReadFile(path.to_owned(), e))?;
|
||||
|
||||
let key = RsaPublicKey::from_public_key_pem(&data)?;
|
||||
let key = RsaPublicKey::from_public_key_pem(&data).map_err(Error::LoadPubKey)?;
|
||||
|
||||
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)?;
|
||||
pub fn write_pem_public_key(path: &Path, mut writer: impl Write, key: &RsaPublicKey) -> Result<()> {
|
||||
let data = key
|
||||
.to_public_key_pem(LineEnding::LF)
|
||||
.map_err(Error::SavePubKey)?;
|
||||
|
||||
writer.write_all(data.as_bytes())?;
|
||||
writer
|
||||
.write_all(data.as_bytes())
|
||||
.map_err(|e| Error::WriteFile(path.to_owned(), e))?;
|
||||
|
||||
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);
|
||||
let reader = File::open(path).map_err(|e| Error::ReadFile(path.to_owned(), e))?;
|
||||
|
||||
read_pem_public_key(reader)
|
||||
read_pem_public_key(path, reader)
|
||||
}
|
||||
|
||||
/// Write PEM-encoded PKCS8 public key to a file.
|
||||
pub fn write_pem_public_key_file(path: &Path, key: &RsaPublicKey) -> Result<()> {
|
||||
let file = File::create(path)?;
|
||||
let writer = BufWriter::new(file);
|
||||
let writer = File::create(path).map_err(|e| Error::WriteFile(path.to_owned(), e))?;
|
||||
|
||||
write_pem_public_key(writer, key)
|
||||
write_pem_public_key(path, writer, key)
|
||||
}
|
||||
|
||||
/// Read PEM-encoded PKCS8 private key from a reader.
|
||||
pub fn read_pem_key(mut reader: impl Read, source: &PassphraseSource) -> Result<RsaPrivateKey> {
|
||||
pub fn read_pem_key(
|
||||
path: &Path,
|
||||
mut reader: impl Read,
|
||||
source: &PassphraseSource,
|
||||
) -> Result<RsaPrivateKey> {
|
||||
let mut data = String::new();
|
||||
reader.read_to_string(&mut data)?;
|
||||
reader
|
||||
.read_to_string(&mut data)
|
||||
.map_err(|e| Error::ReadFile(path.to_owned(), e))?;
|
||||
|
||||
if data.contains("ENCRYPTED") {
|
||||
let passphrase = source.acquire(false)?;
|
||||
@@ -530,6 +572,7 @@ pub fn read_pem_key(mut reader: impl Read, source: &PassphraseSource) -> Result<
|
||||
|
||||
/// Write PEM-encoded PKCS8 private key to a writer.
|
||||
pub fn write_pem_key(
|
||||
path: &Path,
|
||||
mut writer: impl Write,
|
||||
key: &RsaPrivateKey,
|
||||
source: &PassphraseSource,
|
||||
@@ -572,20 +615,24 @@ pub fn write_pem_key(
|
||||
.encrypt_with_params(pbes2_params, passphrase)
|
||||
.map_err(Error::SaveKeyEncrypted)?;
|
||||
|
||||
secret_doc.to_pem(EncryptedPrivateKeyInfo::PEM_LABEL, LineEnding::LF)?
|
||||
secret_doc
|
||||
.to_pem(EncryptedPrivateKeyInfo::PEM_LABEL, LineEnding::LF)
|
||||
.map_err(pkcs8::Error::Asn1)
|
||||
.map_err(Error::SaveKeyEncrypted)?
|
||||
};
|
||||
|
||||
writer.write_all(data.as_bytes())?;
|
||||
writer
|
||||
.write_all(data.as_bytes())
|
||||
.map_err(|e| Error::WriteFile(path.to_owned(), e))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Read PEM-encoded PKCS8 private key from a file.
|
||||
pub fn read_pem_key_file(path: &Path, source: &PassphraseSource) -> Result<RsaPrivateKey> {
|
||||
let file = File::open(path)?;
|
||||
let reader = BufReader::new(file);
|
||||
let reader = File::open(path).map_err(|e| Error::ReadFile(path.to_owned(), e))?;
|
||||
|
||||
read_pem_key(reader, source)
|
||||
read_pem_key(path, reader, source)
|
||||
}
|
||||
|
||||
/// Save PEM-encoded PKCS8 private key to a file.
|
||||
@@ -605,16 +652,18 @@ pub fn write_pem_key_file(
|
||||
options.mode(0o600);
|
||||
}
|
||||
|
||||
let file = options.open(path)?;
|
||||
let writer = BufWriter::new(file);
|
||||
let writer = options
|
||||
.open(path)
|
||||
.map_err(|e| Error::WriteFile(path.to_owned(), e))?;
|
||||
|
||||
write_pem_key(writer, key, source)
|
||||
write_pem_key(path, writer, key, source)
|
||||
}
|
||||
|
||||
/// Get the RSA public key from a certificate.
|
||||
pub fn get_public_key(cert: &Certificate) -> Result<RsaPublicKey> {
|
||||
let public_key =
|
||||
RsaPublicKey::try_from(cert.tbs_certificate.subject_public_key_info.owned_to_ref())?;
|
||||
RsaPublicKey::try_from(cert.tbs_certificate.subject_public_key_info.owned_to_ref())
|
||||
.map_err(Error::LoadPubKey)?;
|
||||
|
||||
Ok(public_key)
|
||||
}
|
||||
@@ -628,27 +677,26 @@ pub fn cert_matches_key(cert: &Certificate, key: &RsaSigningKey) -> Result<bool>
|
||||
|
||||
/// Parse a CMS [`SignedData`] structure from raw DER-encoded data.
|
||||
pub fn parse_cms(data: &[u8]) -> Result<SignedData> {
|
||||
let ci = ContentInfo::from_der(data)?;
|
||||
let sd = ci.content.decode_as::<SignedData>()?;
|
||||
let ci = ContentInfo::from_der(data).map_err(Error::CmsParse)?;
|
||||
let sd = ci
|
||||
.content
|
||||
.decode_as::<SignedData>()
|
||||
.map_err(Error::CmsParse)?;
|
||||
|
||||
Ok(sd)
|
||||
}
|
||||
|
||||
/// Get a list of all standard X509 certificates contained within a
|
||||
/// Get an iterator to all standard X509 certificates contained within a
|
||||
/// [`SignedData`] structure.
|
||||
pub fn get_cms_certs(sd: &SignedData) -> Vec<Certificate> {
|
||||
sd.certificates.as_ref().map_or_else(Vec::new, |certs| {
|
||||
certs
|
||||
.0
|
||||
.iter()
|
||||
.filter_map(|cc| {
|
||||
if let CertificateChoices::Certificate(c) = cc {
|
||||
Some(c.clone())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
pub fn iter_cms_certs(sd: &SignedData) -> impl Iterator<Item = &Certificate> {
|
||||
sd.certificates.iter().flat_map(|certs| {
|
||||
certs.0.iter().filter_map(|cc| {
|
||||
if let CertificateChoices::Certificate(c) = cc {
|
||||
Some(c)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -671,14 +719,16 @@ pub fn cms_sign_external(
|
||||
|
||||
let signed_data = SignedData {
|
||||
version: CmsVersion::V1,
|
||||
digest_algorithms: DigestAlgorithmIdentifiers::try_from(vec![digest_algorithm.clone()])?,
|
||||
digest_algorithms: DigestAlgorithmIdentifiers::try_from(vec![digest_algorithm.clone()])
|
||||
.map_err(Error::CmsSign)?,
|
||||
encap_content_info: EncapsulatedContentInfo {
|
||||
econtent_type: const_oid::db::rfc5911::ID_DATA,
|
||||
econtent: None,
|
||||
},
|
||||
certificates: Some(CertificateSet::try_from(vec![
|
||||
CertificateChoices::Certificate(cert.clone()),
|
||||
])?),
|
||||
certificates: Some(
|
||||
CertificateSet::try_from(vec![CertificateChoices::Certificate(cert.clone())])
|
||||
.map_err(Error::CmsSign)?,
|
||||
),
|
||||
crls: None,
|
||||
signer_infos: SignerInfos::try_from(vec![SignerInfo {
|
||||
version: CmsVersion::V1,
|
||||
@@ -692,14 +742,15 @@ pub fn cms_sign_external(
|
||||
oid: const_oid::db::rfc5912::SHA_256_WITH_RSA_ENCRYPTION,
|
||||
parameters: None,
|
||||
},
|
||||
signature: SignatureValue::new(signature)?,
|
||||
signature: SignatureValue::new(signature).map_err(Error::CmsSign)?,
|
||||
unsigned_attrs: None,
|
||||
}])?,
|
||||
}])
|
||||
.map_err(Error::CmsSign)?,
|
||||
};
|
||||
|
||||
let signed_data = ContentInfo {
|
||||
content_type: const_oid::db::rfc5911::ID_SIGNED_DATA,
|
||||
content: Any::encode_from(&signed_data)?,
|
||||
content: Any::encode_from(&signed_data).map_err(Error::CmsSign)?,
|
||||
};
|
||||
|
||||
Ok(signed_data)
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2023 Andrew Gunnerson
|
||||
* SPDX-License-Identifier: GPL-3.0-only
|
||||
*/
|
||||
// SPDX-FileCopyrightText: 2023 Andrew Gunnerson
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use std::{fmt, marker::PhantomData};
|
||||
|
||||
@@ -68,7 +66,7 @@ where
|
||||
{
|
||||
struct EscapedStrVisitor<T>(PhantomData<T>);
|
||||
|
||||
impl<'de, T> Visitor<'de> for EscapedStrVisitor<T>
|
||||
impl<T> Visitor<'_> for EscapedStrVisitor<T>
|
||||
where
|
||||
T: FromEscaped,
|
||||
<T as FromEscaped>::Error: fmt::Display,
|
||||
|
||||
+799
-504
File diff suppressed because it is too large
Load Diff
+655
-434
File diff suppressed because it is too large
Load Diff
@@ -1,11 +1,8 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2023 Andrew Gunnerson
|
||||
* SPDX-License-Identifier: GPL-3.0-only
|
||||
*/
|
||||
// SPDX-FileCopyrightText: 2023-2024 Andrew Gunnerson
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use std::io::{self, Read, Seek, Write};
|
||||
|
||||
use byteorder::{LittleEndian, WriteBytesExt};
|
||||
use flate2::{read::GzDecoder, write::GzEncoder, Compression};
|
||||
use liblzma::{
|
||||
read::XzDecoder,
|
||||
@@ -16,6 +13,8 @@ use lz4_flex::frame::FrameDecoder;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use thiserror::Error;
|
||||
|
||||
use crate::stream::ReadFixedSizeExt;
|
||||
|
||||
static GZIP_MAGIC: &[u8; 2] = b"\x1f\x8b";
|
||||
static LZ4_LEGACY_MAGIC: &[u8; 4] = b"\x02\x21\x4c\x18";
|
||||
static XZ_MAGIC: &[u8; 6] = b"\xfd\x37\x7a\x58\x5a\x00";
|
||||
@@ -24,10 +23,12 @@ static XZ_MAGIC: &[u8; 6] = b"\xfd\x37\x7a\x58\x5a\x00";
|
||||
pub enum Error {
|
||||
#[error("Unknown compression format")]
|
||||
UnknownFormat,
|
||||
#[error("XZ stream error")]
|
||||
XzStream(#[from] liblzma::stream::Error),
|
||||
#[error("I/O error")]
|
||||
Io(#[from] io::Error),
|
||||
#[error("I/O error when autodetecting compression format")]
|
||||
AutoDetect(#[source] io::Error),
|
||||
#[error("Failed to initialize legacy LZ4 encoder")]
|
||||
Lz4Init(#[source] io::Error),
|
||||
#[error("Failed to initialize XZ encoder")]
|
||||
XzInit(#[source] liblzma::stream::Error),
|
||||
}
|
||||
|
||||
type Result<T> = std::result::Result<T, Error>;
|
||||
@@ -61,7 +62,7 @@ impl<W: Write> Lz4LegacyEncoder<W> {
|
||||
let compressed = lz4_flex::block::compress(&self.buf[..self.n_filled]);
|
||||
|
||||
let writer = self.writer.as_mut().unwrap();
|
||||
writer.write_u32::<LittleEndian>(compressed.len() as u32)?;
|
||||
writer.write_all(&(compressed.len() as u32).to_le_bytes())?;
|
||||
writer.write_all(&compressed)?;
|
||||
|
||||
self.n_filled = 0;
|
||||
@@ -122,10 +123,9 @@ pub enum CompressedReader<R: Read> {
|
||||
|
||||
impl<R: Read + Seek> CompressedReader<R> {
|
||||
pub fn new(mut reader: R, raw_if_unknown: bool) -> Result<Self> {
|
||||
let mut magic = [0u8; 6];
|
||||
reader.read_exact(&mut magic)?;
|
||||
let magic = reader.read_array_exact::<6>().map_err(Error::AutoDetect)?;
|
||||
|
||||
reader.rewind()?;
|
||||
reader.rewind().map_err(Error::AutoDetect)?;
|
||||
|
||||
if &magic[0..2] == GZIP_MAGIC {
|
||||
Ok(Self::Gzip(GzDecoder::new(reader)))
|
||||
@@ -184,10 +184,13 @@ impl<W: Write> CompressedWriter<W> {
|
||||
CompressedFormat::Gzip => {
|
||||
Ok(Self::Gzip(GzEncoder::new(writer, Compression::default())))
|
||||
}
|
||||
CompressedFormat::Lz4Legacy => Ok(Self::Lz4Legacy(Lz4LegacyEncoder::new(writer)?)),
|
||||
CompressedFormat::Lz4Legacy => {
|
||||
let encoder = Lz4LegacyEncoder::new(writer).map_err(Error::Lz4Init)?;
|
||||
Ok(Self::Lz4Legacy(encoder))
|
||||
}
|
||||
CompressedFormat::Xz => {
|
||||
// Some kernels are compiled without support for the default CRC64.
|
||||
let stream = Stream::new_easy_encoder(6, Check::Crc32)?;
|
||||
let stream = Stream::new_easy_encoder(6, Check::Crc32).map_err(Error::XzInit)?;
|
||||
Ok(Self::Xz(XzEncoder::new_stream(writer, stream)))
|
||||
}
|
||||
}
|
||||
|
||||
+165
-103
@@ -1,7 +1,5 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2023 Andrew Gunnerson
|
||||
* SPDX-License-Identifier: GPL-3.0-only
|
||||
*/
|
||||
// SPDX-FileCopyrightText: 2023-2024 Andrew Gunnerson
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use std::{
|
||||
collections::{HashMap, HashSet},
|
||||
@@ -15,6 +13,8 @@ use bstr::ByteSlice;
|
||||
use num_traits::{ToPrimitive, Zero};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use thiserror::Error;
|
||||
use zerocopy::{FromBytes, IntoBytes};
|
||||
use zerocopy_derive::{FromBytes, Immutable, IntoBytes, KnownLayout, Unaligned};
|
||||
|
||||
use crate::{
|
||||
escape,
|
||||
@@ -50,55 +50,94 @@ const VEC_CAP_THRESHOLD: usize = 16384;
|
||||
pub enum Error {
|
||||
#[error("Unknown magic: {0:?}")]
|
||||
UnknownMagic([u8; 6]),
|
||||
#[error("Path is not NULL-terminated: {:?}", .0.as_bstr())]
|
||||
PathNotNullTerminated(Vec<u8>),
|
||||
#[error("Hard links are not supported: {:?}", .0.as_bstr())]
|
||||
HardLinksNotSupported(Vec<u8>),
|
||||
#[error("Entry of type {0} should not have data: {:?}", .1.as_bstr())]
|
||||
#[error("Entry of type {0} should not have data: {path:?}", path = .1.as_bstr())]
|
||||
EntryHasData(CpioEntryType, Vec<u8>),
|
||||
#[error("No inodes available for device {0:x},{1:x}")]
|
||||
DeviceFull(u32, u32),
|
||||
#[error("{0:?} field exceeds integer bounds")]
|
||||
IntegerTooLarge(&'static str),
|
||||
#[error("I/O error")]
|
||||
Io(#[from] io::Error),
|
||||
#[error("No inodes available for device {major:x},{minor:x}")]
|
||||
DeviceFull { major: u32, minor: u32 },
|
||||
#[error("{0:?} overflowed integer bounds during calculations")]
|
||||
IntOverflow(&'static str),
|
||||
#[error("{0:?} contains invalid hex integer")]
|
||||
InvalidHexInt(&'static str, #[source] InvalidHexCharError),
|
||||
#[error("Failed to read cpio data: {0}")]
|
||||
DataRead(&'static str, #[source] io::Error),
|
||||
#[error("Failed to write cpio data: {0}")]
|
||||
DataWrite(&'static str, #[source] io::Error),
|
||||
}
|
||||
|
||||
type Result<T> = std::result::Result<T, Error>;
|
||||
|
||||
/// Read u32 formatted as an ASCII 8-char wide hex string.
|
||||
fn read_int(mut reader: impl Read) -> io::Result<u32> {
|
||||
let mut buf = [0u8; 8];
|
||||
reader.read_exact(&mut buf)?;
|
||||
#[derive(Debug, Error)]
|
||||
#[error("{0:?}: Invalid hex char: {1:?}")]
|
||||
pub struct InvalidHexCharError(RawHexU32, char);
|
||||
|
||||
let mut value = 0;
|
||||
/// ASCII-encoded hex integer value used in cpio header fields.
|
||||
#[derive(Clone, Copy, FromBytes, IntoBytes, KnownLayout, Immutable, Unaligned)]
|
||||
#[repr(packed)]
|
||||
struct RawHexU32([u8; 8]);
|
||||
|
||||
for b in buf {
|
||||
let c = b as char;
|
||||
let digit = c.to_digit(16).ok_or_else(|| {
|
||||
io::Error::new(
|
||||
io::ErrorKind::InvalidData,
|
||||
format!("{:?}: Invalid hex char: {c}", buf.as_bstr()),
|
||||
)
|
||||
})?;
|
||||
|
||||
value <<= 4;
|
||||
value |= digit;
|
||||
impl fmt::Debug for RawHexU32 {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{:?}", self.0.as_bstr())
|
||||
}
|
||||
|
||||
Ok(value)
|
||||
}
|
||||
|
||||
/// Write u32 formatted as an ASCII 8-char wide hex string.
|
||||
fn write_int(mut writer: impl Write, mut value: u32) -> io::Result<()> {
|
||||
let mut buf = [b'0'; 8];
|
||||
let mut index = 7;
|
||||
#[allow(clippy::fallible_impl_from)]
|
||||
impl From<u32> for RawHexU32 {
|
||||
fn from(mut value: u32) -> Self {
|
||||
let mut buf = [b'0'; 8];
|
||||
let mut index = 7;
|
||||
|
||||
while value != 0 {
|
||||
buf[index] = char::from_digit(value & 0xf, 16).unwrap() as u8;
|
||||
value >>= 4;
|
||||
index -= 1;
|
||||
while value != 0 {
|
||||
buf[index] = char::from_digit(value & 0xf, 16).unwrap() as u8;
|
||||
value >>= 4;
|
||||
index -= 1;
|
||||
}
|
||||
|
||||
Self(buf)
|
||||
}
|
||||
}
|
||||
|
||||
writer.write_all(&buf)
|
||||
impl TryFrom<RawHexU32> for u32 {
|
||||
type Error = InvalidHexCharError;
|
||||
|
||||
fn try_from(raw_value: RawHexU32) -> std::result::Result<Self, Self::Error> {
|
||||
let mut value = 0;
|
||||
|
||||
for b in raw_value.0 {
|
||||
let c = b as char;
|
||||
let digit = c.to_digit(16).ok_or(InvalidHexCharError(raw_value, c))?;
|
||||
|
||||
value <<= 4;
|
||||
value |= digit;
|
||||
}
|
||||
|
||||
Ok(value)
|
||||
}
|
||||
}
|
||||
|
||||
/// Raw on-disk layout for the cpio header.
|
||||
#[derive(Clone, Copy, FromBytes, IntoBytes, KnownLayout, Immutable, Unaligned)]
|
||||
#[repr(packed)]
|
||||
struct RawHeader {
|
||||
/// Magic value. This should be equal to [`MAGIC_NEW`] or [`MAGIC_NEW_CRC`].
|
||||
magic: [u8; 6],
|
||||
inode: RawHexU32,
|
||||
mode: RawHexU32,
|
||||
uid: RawHexU32,
|
||||
gid: RawHexU32,
|
||||
nlink: RawHexU32,
|
||||
mtime: RawHexU32,
|
||||
file_size: RawHexU32,
|
||||
dev_maj: RawHexU32,
|
||||
dev_min: RawHexU32,
|
||||
rdev_maj: RawHexU32,
|
||||
rdev_min: RawHexU32,
|
||||
path_size: RawHexU32,
|
||||
crc32: RawHexU32,
|
||||
}
|
||||
|
||||
/// Read a chunk of bytes from the reader. If `size` is less than
|
||||
@@ -213,17 +252,14 @@ impl CpioEntryData {
|
||||
pub fn size(&self) -> Result<u32> {
|
||||
let size = match self {
|
||||
Self::Size(s) => *s,
|
||||
Self::Data(d) => d
|
||||
.len()
|
||||
.to_u32()
|
||||
.ok_or_else(|| Error::IntegerTooLarge("data_size"))?,
|
||||
Self::Data(d) => d.len().to_u32().ok_or(Error::IntOverflow("data_size"))?,
|
||||
};
|
||||
|
||||
Ok(size)
|
||||
}
|
||||
|
||||
fn is_size(&self) -> bool {
|
||||
matches!(self, CpioEntryData::Size(_))
|
||||
matches!(self, Self::Size(_))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -391,41 +427,45 @@ impl<R: Read> FromReader<R> for CpioEntry {
|
||||
fn from_reader(reader: R) -> Result<Self> {
|
||||
let mut reader = CountingReader::new(reader);
|
||||
|
||||
let mut magic = [0u8; 6];
|
||||
reader.read_exact(&mut magic)?;
|
||||
let header =
|
||||
RawHeader::read_from_io(&mut reader).map_err(|e| Error::DataRead("header", e))?;
|
||||
|
||||
if magic != *MAGIC_NEW && magic != *MAGIC_NEW_CRC {
|
||||
return Err(Error::UnknownMagic(magic));
|
||||
if header.magic != *MAGIC_NEW && header.magic != *MAGIC_NEW_CRC {
|
||||
return Err(Error::UnknownMagic(header.magic));
|
||||
}
|
||||
|
||||
let inode = read_int(&mut reader)?;
|
||||
let mode = read_int(&mut reader)?;
|
||||
let uid = read_int(&mut reader)?;
|
||||
let gid = read_int(&mut reader)?;
|
||||
let nlink = read_int(&mut reader)?;
|
||||
let mtime = read_int(&mut reader)?;
|
||||
let file_size = read_int(&mut reader)?;
|
||||
let dev_maj = read_int(&mut reader)?;
|
||||
let dev_min = read_int(&mut reader)?;
|
||||
let rdev_maj = read_int(&mut reader)?;
|
||||
let rdev_min = read_int(&mut reader)?;
|
||||
let path_size = read_int(&mut reader)?;
|
||||
let crc32 = read_int(&mut reader)?;
|
||||
macro_rules! get_field {
|
||||
($name:ident) => {
|
||||
let $name = u32::try_from(header.$name)
|
||||
.map_err(|e| Error::InvalidHexInt(stringify!($name), e))?;
|
||||
};
|
||||
}
|
||||
|
||||
get_field!(inode);
|
||||
get_field!(mode);
|
||||
get_field!(uid);
|
||||
get_field!(gid);
|
||||
get_field!(nlink);
|
||||
get_field!(mtime);
|
||||
get_field!(file_size);
|
||||
get_field!(dev_maj);
|
||||
get_field!(dev_min);
|
||||
get_field!(rdev_maj);
|
||||
get_field!(rdev_min);
|
||||
get_field!(path_size);
|
||||
get_field!(crc32);
|
||||
|
||||
let mut path = read_data(
|
||||
&mut reader,
|
||||
path_size.to_usize().unwrap(),
|
||||
&AtomicBool::new(false),
|
||||
)?;
|
||||
)
|
||||
.map_err(|e| Error::DataRead("path", e))?;
|
||||
if path.last() != Some(&b'\0') {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidData,
|
||||
"Filename is not NULL-terminated",
|
||||
)
|
||||
.into());
|
||||
return Err(Error::PathNotNullTerminated(path));
|
||||
}
|
||||
path.pop();
|
||||
padding::read_discard(&mut reader, 4)?;
|
||||
padding::read_discard(&mut reader, 4).map_err(|e| Error::DataRead("path_padding", e))?;
|
||||
|
||||
let file_type = CpioEntryType::from_mode(mode);
|
||||
let data = match file_type {
|
||||
@@ -437,8 +477,10 @@ impl<R: Read> FromReader<R> for CpioEntry {
|
||||
&mut reader,
|
||||
file_size.to_usize().unwrap(),
|
||||
&AtomicBool::new(false),
|
||||
)?;
|
||||
padding::read_discard(&mut reader, 4)?;
|
||||
)
|
||||
.map_err(|e| Error::DataRead("content", e))?;
|
||||
padding::read_discard(&mut reader, 4)
|
||||
.map_err(|e| Error::DataRead("content_padding", e))?;
|
||||
|
||||
CpioEntryData::Data(content)
|
||||
}
|
||||
@@ -477,7 +519,7 @@ impl<W: Write> ToWriter<W> for CpioEntry {
|
||||
.len()
|
||||
.checked_add(1)
|
||||
.and_then(|s| s.to_u32())
|
||||
.ok_or_else(|| Error::IntegerTooLarge("path_size"))?;
|
||||
.ok_or(Error::IntOverflow("path_size"))?;
|
||||
|
||||
let file_size = self.data.size()?;
|
||||
if file_size != 0
|
||||
@@ -487,35 +529,47 @@ impl<W: Write> ToWriter<W> for CpioEntry {
|
||||
return Err(Error::EntryHasData(self.file_type, self.path.clone()));
|
||||
}
|
||||
|
||||
if self.crc32 == 0 {
|
||||
writer.write_all(MAGIC_NEW)?;
|
||||
} else {
|
||||
writer.write_all(MAGIC_NEW_CRC)?;
|
||||
}
|
||||
|
||||
let mode = self.file_type.to_mode() | u32::from(self.file_mode & 0o7777);
|
||||
|
||||
write_int(&mut writer, self.inode)?;
|
||||
write_int(&mut writer, mode)?;
|
||||
write_int(&mut writer, self.uid)?;
|
||||
write_int(&mut writer, self.gid)?;
|
||||
write_int(&mut writer, self.nlink)?;
|
||||
write_int(&mut writer, self.mtime)?;
|
||||
write_int(&mut writer, file_size)?;
|
||||
write_int(&mut writer, self.dev_maj)?;
|
||||
write_int(&mut writer, self.dev_min)?;
|
||||
write_int(&mut writer, self.rdev_maj)?;
|
||||
write_int(&mut writer, self.rdev_min)?;
|
||||
write_int(&mut writer, path_size)?;
|
||||
write_int(&mut writer, self.crc32)?;
|
||||
let raw_header = RawHeader {
|
||||
magic: if self.crc32 == 0 {
|
||||
*MAGIC_NEW
|
||||
} else {
|
||||
*MAGIC_NEW_CRC
|
||||
},
|
||||
inode: self.inode.into(),
|
||||
mode: mode.into(),
|
||||
uid: self.uid.into(),
|
||||
gid: self.gid.into(),
|
||||
nlink: self.nlink.into(),
|
||||
mtime: self.mtime.into(),
|
||||
file_size: file_size.into(),
|
||||
dev_maj: self.dev_maj.into(),
|
||||
dev_min: self.dev_min.into(),
|
||||
rdev_maj: self.rdev_maj.into(),
|
||||
rdev_min: self.rdev_min.into(),
|
||||
path_size: path_size.into(),
|
||||
crc32: self.crc32.into(),
|
||||
};
|
||||
|
||||
writer.write_all(&self.path)?;
|
||||
writer.write_zeros_exact(1)?;
|
||||
padding::write_zeros(&mut writer, 4)?;
|
||||
raw_header
|
||||
.write_to_io(&mut writer)
|
||||
.map_err(|e| Error::DataWrite("header", e))?;
|
||||
|
||||
writer
|
||||
.write_all(&self.path)
|
||||
.map_err(|e| Error::DataWrite("path", e))?;
|
||||
writer
|
||||
.write_zeros_exact(1)
|
||||
.map_err(|e| Error::DataWrite("path", e))?;
|
||||
padding::write_zeros(&mut writer, 4).map_err(|e| Error::DataWrite("path_padding", e))?;
|
||||
|
||||
if let CpioEntryData::Data(d) = &self.data {
|
||||
writer.write_all(d)?;
|
||||
padding::write_zeros(&mut writer, 4)?;
|
||||
writer
|
||||
.write_all(d)
|
||||
.map_err(|e| Error::DataWrite("content", e))?;
|
||||
padding::write_zeros(&mut writer, 4)
|
||||
.map_err(|e| Error::DataWrite("content_padding", e))?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@@ -559,7 +613,8 @@ impl<R: Read> CpioReader<R> {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
self.skip_data()?;
|
||||
self.skip_data()
|
||||
.map_err(|e| Error::DataRead("content", e))?;
|
||||
|
||||
let entry = CpioEntry::from_reader(&mut self.reader)?;
|
||||
|
||||
@@ -624,7 +679,8 @@ impl<W: Write> CpioWriter<W> {
|
||||
}
|
||||
|
||||
pub fn start_entry(&mut self, entry: &CpioEntry) -> Result<()> {
|
||||
self.finish_entry()?;
|
||||
self.finish_entry()
|
||||
.map_err(|e| Error::DataWrite("content", e))?;
|
||||
|
||||
entry.to_writer(&mut self.writer)?;
|
||||
|
||||
@@ -638,13 +694,15 @@ impl<W: Write> CpioWriter<W> {
|
||||
}
|
||||
|
||||
pub fn finish(mut self) -> Result<W> {
|
||||
self.finish_entry()?;
|
||||
self.finish_entry()
|
||||
.map_err(|e| Error::DataWrite("content", e))?;
|
||||
|
||||
self.start_entry(&CpioEntry::new_trailer())?;
|
||||
|
||||
// Pad until the end of the block.
|
||||
if self.pad_to_block_size {
|
||||
padding::write_zeros(&mut self.writer, IO_BLOCK_SIZE)?;
|
||||
padding::write_zeros(&mut self.writer, IO_BLOCK_SIZE)
|
||||
.map_err(|e| Error::DataWrite("block_padding", e))?;
|
||||
}
|
||||
|
||||
Ok(self.writer.finish().0)
|
||||
@@ -682,14 +740,15 @@ pub fn load(
|
||||
let mut entries = vec![];
|
||||
|
||||
while let Some(mut entry) = cpio_reader.next_entry()? {
|
||||
stream::check_cancel(cancel_signal)?;
|
||||
stream::check_cancel(cancel_signal).map_err(|e| Error::DataRead("entry", e))?;
|
||||
|
||||
if entry.file_type != CpioEntryType::Directory && entry.nlink > 1 {
|
||||
return Err(Error::HardLinksNotSupported(entry.path.clone()));
|
||||
return Err(Error::HardLinksNotSupported(entry.path));
|
||||
}
|
||||
|
||||
if let CpioEntryData::Size(s) = entry.data {
|
||||
let data = read_data(&mut cpio_reader, s.to_usize().unwrap(), cancel_signal)?;
|
||||
let data = read_data(&mut cpio_reader, s.to_usize().unwrap(), cancel_signal)
|
||||
.map_err(|e| Error::DataWrite("data", e))?;
|
||||
entry.data = CpioEntryData::Data(data);
|
||||
}
|
||||
|
||||
@@ -745,7 +804,10 @@ pub fn assign_inodes(entries: &mut [CpioEntry], missing_only: bool) -> Result<()
|
||||
|
||||
while set.contains(&unused) {
|
||||
if unused == *last {
|
||||
return Err(Error::DeviceFull(entry.dev_maj, entry.dev_min));
|
||||
return Err(Error::DeviceFull {
|
||||
major: entry.dev_maj,
|
||||
minor: entry.dev_min,
|
||||
});
|
||||
}
|
||||
|
||||
unused = next_non_zero(unused);
|
||||
@@ -769,7 +831,7 @@ pub fn save(
|
||||
let mut cpio_writer = CpioWriter::new(writer, pad_to_block_size);
|
||||
|
||||
for entry in entries {
|
||||
stream::check_cancel(cancel_signal)?;
|
||||
stream::check_cancel(cancel_signal).map_err(|e| Error::DataWrite("entry", e))?;
|
||||
|
||||
cpio_writer.start_entry(entry)?;
|
||||
// CpioEntryData::Data will have already been written.
|
||||
|
||||
+132
-97
@@ -1,33 +1,32 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2023 Andrew Gunnerson
|
||||
* SPDX-License-Identifier: GPL-3.0-only
|
||||
*/
|
||||
// SPDX-FileCopyrightText: 2023-2024 Andrew Gunnerson
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use std::{
|
||||
collections::HashSet,
|
||||
fmt,
|
||||
io::{self, Cursor, Read, Seek, SeekFrom, Write},
|
||||
io::{self, Read, Seek, SeekFrom, Write},
|
||||
mem,
|
||||
ops::Range,
|
||||
sync::atomic::AtomicBool,
|
||||
};
|
||||
|
||||
use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt};
|
||||
use num_traits::ToPrimitive;
|
||||
use rayon::{
|
||||
prelude::{IndexedParallelIterator, ParallelIterator},
|
||||
slice::{ParallelSlice, ParallelSliceMut},
|
||||
};
|
||||
use thiserror::Error;
|
||||
use zerocopy::{little_endian, FromBytes, IntoBytes};
|
||||
use zerocopy_derive::{FromBytes, Immutable, IntoBytes, KnownLayout, Unaligned};
|
||||
|
||||
use crate::{
|
||||
format::verityrs,
|
||||
stream::{self, FromReader, ReadSeekReopen, ToWriter, WriteSeekReopen, WriteZerosExt},
|
||||
util::{self, NumBytes},
|
||||
util::{self, NumBytes, OutOfBoundsError},
|
||||
};
|
||||
|
||||
// Not to be confused with the 255-byte RS block size.
|
||||
const FEC_BLOCK_SIZE: usize = 4096;
|
||||
const FEC_HEADER_SIZE: usize = 60;
|
||||
const FEC_MAGIC: u32 = 0xFECFECFE;
|
||||
const FEC_VERSION: u32 = 0;
|
||||
|
||||
@@ -66,9 +65,17 @@ pub enum Error {
|
||||
#[error("Expected FEC digest {expected}, but have {actual}")]
|
||||
InvalidFecDigest { expected: String, actual: String },
|
||||
#[error("{0:?} field is out of bounds")]
|
||||
FieldOutOfBounds(&'static str),
|
||||
#[error("I/O error")]
|
||||
Io(#[from] io::Error),
|
||||
IntOutOfBounds(&'static str, #[source] OutOfBoundsError),
|
||||
#[error("{0:?} overflowed integer bounds during calculations")]
|
||||
IntOverflow(&'static str),
|
||||
#[error("Failed to reopen input file")]
|
||||
InputReopen(#[source] io::Error),
|
||||
#[error("Failed to reopen output file")]
|
||||
OutputReopen(#[source] io::Error),
|
||||
#[error("Failed to read FEC data: {0}")]
|
||||
DataRead(&'static str, #[source] io::Error),
|
||||
#[error("Failed to write FEC data: {0}")]
|
||||
DataWrite(&'static str, #[source] io::Error),
|
||||
}
|
||||
|
||||
type Result<T> = std::result::Result<T, Error>;
|
||||
@@ -96,7 +103,7 @@ impl Codeword {
|
||||
&mut self.data[..usize::from(self.rs_k)]
|
||||
}
|
||||
|
||||
fn parity(&mut self) -> &[u8] {
|
||||
fn parity(&self) -> &[u8] {
|
||||
&self.data[usize::from(self.rs_k)..]
|
||||
}
|
||||
|
||||
@@ -157,10 +164,11 @@ impl Fec {
|
||||
input: file_size,
|
||||
block: block_size,
|
||||
});
|
||||
} else if block_size > FEC_MAX_BLOCK_SIZE {
|
||||
return Err(Error::FieldOutOfBounds("block_size"));
|
||||
}
|
||||
|
||||
util::check_bounds(block_size, ..=FEC_MAX_BLOCK_SIZE)
|
||||
.map_err(|e| Error::IntOutOfBounds("block_size", e))?;
|
||||
|
||||
let rs_k = 255 - parity;
|
||||
if !verityrs::FN_ENCODE.contains_key(&rs_k) {
|
||||
return Err(Error::UnsupportedParity(parity));
|
||||
@@ -174,11 +182,11 @@ impl Fec {
|
||||
.checked_mul(u64::from(parity))
|
||||
.and_then(|s| s.checked_mul(u64::from(block_size)))
|
||||
.and_then(|s| s.to_usize())
|
||||
.ok_or_else(|| Error::FieldOutOfBounds("fec_data_size"))?;
|
||||
.ok_or(Error::IntOverflow("fec_data_size"))?;
|
||||
rounds
|
||||
.checked_mul(u64::from(rs_k))
|
||||
.and_then(|s| s.checked_mul(u64::from(block_size)))
|
||||
.ok_or_else(|| Error::FieldOutOfBounds("fec_grid_size"))?;
|
||||
.ok_or(Error::IntOverflow("fec_grid_size"))?;
|
||||
|
||||
Ok(Self {
|
||||
file_size,
|
||||
@@ -212,9 +220,8 @@ impl Fec {
|
||||
fn rounds_for_ranges(&self, ranges: &[Range<u64>]) -> Result<HashSet<u64>> {
|
||||
let ranges = util::merge_overlapping(ranges);
|
||||
if let Some(last) = ranges.last() {
|
||||
if last.end > self.file_size {
|
||||
return Err(Error::FieldOutOfBounds("ranges"));
|
||||
}
|
||||
util::check_bounds(last.end, ..=self.file_size)
|
||||
.map_err(|e| Error::IntOutOfBounds("ranges", e))?;
|
||||
}
|
||||
|
||||
let block_size = u64::from(self.block_size);
|
||||
@@ -363,7 +370,9 @@ impl Fec {
|
||||
"FEC buffer length does not match block size",
|
||||
);
|
||||
|
||||
let grid = self.read_round(reader, round)?;
|
||||
let grid = self
|
||||
.read_round(reader, round)
|
||||
.map_err(|e| Error::DataRead("round", e))?;
|
||||
let encode = verityrs::FN_ENCODE[&self.rs_k];
|
||||
let parity = usize::from(self.parity());
|
||||
|
||||
@@ -384,7 +393,9 @@ impl Fec {
|
||||
"FEC buffer length does not match block size",
|
||||
);
|
||||
|
||||
let grid = self.read_round(reader, round)?;
|
||||
let grid = self
|
||||
.read_round(reader, round)
|
||||
.map_err(|e| Error::DataRead("round", e))?;
|
||||
let is_correct = verityrs::FN_IS_CORRECT[&self.rs_k];
|
||||
let parity = usize::from(self.parity());
|
||||
|
||||
@@ -414,7 +425,9 @@ impl Fec {
|
||||
"FEC buffer length does not match block size",
|
||||
);
|
||||
|
||||
let mut grid = self.read_round(reader, round)?;
|
||||
let mut grid = self
|
||||
.read_round(reader, round)
|
||||
.map_err(|e| Error::DataRead("round", e))?;
|
||||
let correct_errors = verityrs::FN_CORRECT_ERRORS[&self.rs_k];
|
||||
let parity = usize::from(self.parity());
|
||||
let mut num_corrected = 0;
|
||||
@@ -432,7 +445,8 @@ impl Fec {
|
||||
}
|
||||
|
||||
if num_corrected > 0 {
|
||||
self.write_round(writer, round, &grid)?;
|
||||
self.write_round(writer, round, &grid)
|
||||
.map_err(|e| Error::DataWrite("round", e))?;
|
||||
}
|
||||
|
||||
Ok(num_corrected)
|
||||
@@ -453,9 +467,9 @@ impl Fec {
|
||||
fec.par_chunks_exact_mut(fec_size / self.rounds as usize)
|
||||
.enumerate()
|
||||
.map(|(round, buf)| -> Result<()> {
|
||||
stream::check_cancel(cancel_signal)?;
|
||||
stream::check_cancel(cancel_signal).map_err(Error::InputReopen)?;
|
||||
|
||||
let reader = input.reopen_boxed()?;
|
||||
let reader = input.reopen_boxed().map_err(Error::InputReopen)?;
|
||||
self.generate_one_round(reader, round as u64, buf)
|
||||
})
|
||||
.collect::<Result<()>>()?;
|
||||
@@ -488,9 +502,9 @@ impl Fec {
|
||||
.enumerate()
|
||||
.filter(|(round, _)| rounds_to_update.contains(&(*round as u64)))
|
||||
.map(|(round, buf)| -> Result<()> {
|
||||
stream::check_cancel(cancel_signal)?;
|
||||
stream::check_cancel(cancel_signal).map_err(Error::InputReopen)?;
|
||||
|
||||
let reader = input.reopen_boxed()?;
|
||||
let reader = input.reopen_boxed().map_err(Error::InputReopen)?;
|
||||
self.generate_one_round(reader, round as u64, buf)
|
||||
})
|
||||
.collect::<Result<()>>()?;
|
||||
@@ -521,9 +535,9 @@ impl Fec {
|
||||
fec.par_chunks_exact(fec_size / self.rounds as usize)
|
||||
.enumerate()
|
||||
.map(|(round, buf)| -> Result<()> {
|
||||
stream::check_cancel(cancel_signal)?;
|
||||
stream::check_cancel(cancel_signal).map_err(Error::InputReopen)?;
|
||||
|
||||
let reader = input.reopen_boxed()?;
|
||||
let reader = input.reopen_boxed().map_err(Error::InputReopen)?;
|
||||
self.verify_one_round(reader, round as u64, buf)
|
||||
})
|
||||
.collect::<Result<()>>()?;
|
||||
@@ -562,10 +576,10 @@ impl Fec {
|
||||
.par_chunks_exact(fec_size / self.rounds as usize)
|
||||
.enumerate()
|
||||
.map(|(round, buf)| -> Result<u64> {
|
||||
stream::check_cancel(cancel_signal)?;
|
||||
stream::check_cancel(cancel_signal).map_err(Error::InputReopen)?;
|
||||
|
||||
let reader = input.reopen_boxed()?;
|
||||
let writer = output.reopen_boxed()?;
|
||||
let reader = input.reopen_boxed().map_err(Error::InputReopen)?;
|
||||
let writer = output.reopen_boxed().map_err(Error::OutputReopen)?;
|
||||
self.repair_one_round(reader, writer, round as u64, buf)
|
||||
})
|
||||
.collect::<Result<Vec<u64>>>()?
|
||||
@@ -576,6 +590,26 @@ impl Fec {
|
||||
}
|
||||
}
|
||||
|
||||
/// Raw on-disk layout for the FEC image header.
|
||||
#[derive(Clone, Copy, FromBytes, IntoBytes, KnownLayout, Immutable, Unaligned)]
|
||||
#[repr(packed)]
|
||||
struct RawHeader {
|
||||
/// Magic value. This should be equal to [`FEC_MAGIC`].
|
||||
magic: little_endian::U32,
|
||||
/// Image version. This should be equal to [`FEC_VERSION`].
|
||||
version: little_endian::U32,
|
||||
/// Size of this [`RawHeader`].
|
||||
header_size: little_endian::U32,
|
||||
/// Number of parity bytes per 255-byte Reed-Solomon codeword.
|
||||
parity: little_endian::U32,
|
||||
/// Size of the FEC data.
|
||||
fec_size: little_endian::U32,
|
||||
/// Size of the actual data.
|
||||
data_size: little_endian::U64,
|
||||
/// SHA-256 digest of the FEC data.
|
||||
digest: [u8; 32],
|
||||
}
|
||||
|
||||
/// A type for reading and writing AOSP's standalone FEC image format.
|
||||
///
|
||||
/// The FEC data parser in this implementation is strict. All header fields,
|
||||
@@ -606,10 +640,10 @@ impl FecImage {
|
||||
parity: u8,
|
||||
cancel_signal: &AtomicBool,
|
||||
) -> Result<Self> {
|
||||
let data_size = {
|
||||
let mut file = input.reopen_boxed()?;
|
||||
file.seek(SeekFrom::End(0))?
|
||||
};
|
||||
let data_size = input
|
||||
.reopen_boxed()
|
||||
.and_then(|mut f| f.seek(SeekFrom::End(0)))
|
||||
.map_err(Error::InputReopen)?;
|
||||
let fec = Fec::new(data_size, FEC_BLOCK_SIZE as u32, parity)?;
|
||||
let fec_data = fec.generate(input, cancel_signal)?;
|
||||
|
||||
@@ -669,26 +703,23 @@ impl FecImage {
|
||||
|
||||
/// Build one instance of the FEC header. The caller is responsible for
|
||||
/// writing it to both of the header locations at the end of the file.
|
||||
fn build_header(&self) -> Result<[u8; FEC_HEADER_SIZE]> {
|
||||
let fec_size = self
|
||||
.fec
|
||||
.len()
|
||||
.to_u32()
|
||||
.ok_or_else(|| Error::FieldOutOfBounds("fec_size"))?;
|
||||
fn build_header(&self) -> Result<RawHeader> {
|
||||
let fec_size: u32 =
|
||||
util::try_cast(self.fec.len()).map_err(|e| Error::IntOutOfBounds("fec_size", e))?;
|
||||
|
||||
let mut writer = Cursor::new([0u8; FEC_HEADER_SIZE]);
|
||||
let digest = aws_lc_rs::digest::digest(&aws_lc_rs::digest::SHA256, &self.fec);
|
||||
|
||||
let digest = ring::digest::digest(&ring::digest::SHA256, &self.fec);
|
||||
let header = RawHeader {
|
||||
magic: FEC_MAGIC.into(),
|
||||
version: FEC_VERSION.into(),
|
||||
header_size: (mem::size_of::<RawHeader>() as u32).into(),
|
||||
parity: u32::from(self.parity).into(),
|
||||
fec_size: fec_size.into(),
|
||||
data_size: self.data_size.into(),
|
||||
digest: digest.as_ref().try_into().unwrap(),
|
||||
};
|
||||
|
||||
writer.write_u32::<LittleEndian>(FEC_MAGIC)?;
|
||||
writer.write_u32::<LittleEndian>(FEC_VERSION)?;
|
||||
writer.write_u32::<LittleEndian>(FEC_HEADER_SIZE as u32)?;
|
||||
writer.write_u32::<LittleEndian>(self.parity.into())?;
|
||||
writer.write_u32::<LittleEndian>(fec_size)?;
|
||||
writer.write_u64::<LittleEndian>(self.data_size)?;
|
||||
writer.write_all(digest.as_ref())?;
|
||||
|
||||
Ok(writer.into_inner())
|
||||
Ok(header)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -699,48 +730,47 @@ impl<R: Read> FromReader<R> for FecImage {
|
||||
// Avoid requiring seekable readers since we need to read everything
|
||||
// into memory anyway.
|
||||
let mut fec = Vec::new();
|
||||
reader.read_to_end(&mut fec)?;
|
||||
reader
|
||||
.read_to_end(&mut fec)
|
||||
.map_err(|e| Error::DataRead("fec", e))?;
|
||||
|
||||
if fec.len() < FEC_BLOCK_SIZE {
|
||||
return Err(Error::DataTooSmall);
|
||||
}
|
||||
|
||||
// Make sure both headers match.
|
||||
let header1_offset = fec.len() - FEC_BLOCK_SIZE;
|
||||
let header2_offset = fec.len() - FEC_HEADER_SIZE;
|
||||
let header1_raw = &fec[header1_offset..header1_offset + FEC_HEADER_SIZE];
|
||||
let header2_raw = &fec[header2_offset..header2_offset + FEC_HEADER_SIZE];
|
||||
|
||||
let (header, _) =
|
||||
RawHeader::ref_from_prefix(&fec[header1_offset..]).map_err(|_| Error::DataTooSmall)?;
|
||||
let header_size = header.header_size.get() as usize;
|
||||
|
||||
if header_size > FEC_BLOCK_SIZE / 2 {
|
||||
// ref_from_prefix() already handles the "too small" case.
|
||||
return Err(Error::InvalidHeaderSize(header.header_size.get()));
|
||||
}
|
||||
|
||||
let header2_offset = fec.len() - header_size;
|
||||
|
||||
// Make sure both headers match, accounting for potential custom fields.
|
||||
let header1_raw = &fec[header1_offset..][..header_size];
|
||||
let header2_raw = &fec[header2_offset..][..header_size];
|
||||
|
||||
if header1_raw != header2_raw {
|
||||
return Err(Error::HeadersDifferent);
|
||||
}
|
||||
|
||||
let mut header_reader = Cursor::new(header1_raw);
|
||||
|
||||
let magic = header_reader.read_u32::<LittleEndian>()?;
|
||||
if magic != FEC_MAGIC {
|
||||
return Err(Error::InvalidHeaderMagic(magic));
|
||||
if header.magic != FEC_MAGIC {
|
||||
return Err(Error::InvalidHeaderMagic(header.magic.get()));
|
||||
}
|
||||
|
||||
let version = header_reader.read_u32::<LittleEndian>()?;
|
||||
if version != FEC_VERSION {
|
||||
return Err(Error::UnsupportedHeaderVersion(version));
|
||||
if header.version != FEC_VERSION {
|
||||
return Err(Error::UnsupportedHeaderVersion(header.version.get()));
|
||||
}
|
||||
|
||||
let header_size = header_reader.read_u32::<LittleEndian>()?;
|
||||
if header_size != FEC_HEADER_SIZE as u32 {
|
||||
return Err(Error::InvalidHeaderSize(header_size));
|
||||
}
|
||||
let parity: u8 =
|
||||
util::try_cast(header.parity.get()).map_err(|e| Error::IntOutOfBounds("parity", e))?;
|
||||
|
||||
let parity = header_reader
|
||||
.read_u32::<LittleEndian>()?
|
||||
.to_u8()
|
||||
.ok_or_else(|| Error::FieldOutOfBounds("parity"))?;
|
||||
|
||||
let fec_size = header_reader
|
||||
.read_u32::<LittleEndian>()?
|
||||
.to_usize()
|
||||
.ok_or_else(|| Error::FieldOutOfBounds("fec_size"))?;
|
||||
let fec_size = header.fec_size.get() as usize;
|
||||
let actual_fec_size = fec.len() - FEC_BLOCK_SIZE;
|
||||
if fec_size != actual_fec_size {
|
||||
return Err(Error::InvalidHeaderFecSize {
|
||||
@@ -749,25 +779,22 @@ impl<R: Read> FromReader<R> for FecImage {
|
||||
});
|
||||
}
|
||||
|
||||
let input_size = header_reader.read_u64::<LittleEndian>()?;
|
||||
let data_size = header.data_size.get();
|
||||
|
||||
let mut digest = [0u8; 32];
|
||||
header_reader.read_exact(&mut digest)?;
|
||||
|
||||
// Chop off headers.
|
||||
fec.resize(fec_size, 0);
|
||||
|
||||
let actual_digest = ring::digest::digest(&ring::digest::SHA256, &fec);
|
||||
if digest != actual_digest.as_ref() {
|
||||
let actual_digest = aws_lc_rs::digest::digest(&aws_lc_rs::digest::SHA256, &fec[..fec_size]);
|
||||
if header.digest != actual_digest.as_ref() {
|
||||
return Err(Error::InvalidFecDigest {
|
||||
expected: hex::encode(digest),
|
||||
expected: hex::encode(header.digest),
|
||||
actual: hex::encode(actual_digest),
|
||||
});
|
||||
}
|
||||
|
||||
// Chop off headers.
|
||||
fec.resize(fec_size, 0);
|
||||
|
||||
Ok(Self {
|
||||
fec,
|
||||
data_size: input_size,
|
||||
data_size,
|
||||
parity,
|
||||
})
|
||||
}
|
||||
@@ -779,10 +806,18 @@ impl<W: Write> ToWriter<W> for FecImage {
|
||||
fn to_writer(&self, mut writer: W) -> Result<()> {
|
||||
let header = self.build_header()?;
|
||||
|
||||
writer.write_all(&self.fec)?;
|
||||
writer.write_all(&header)?;
|
||||
writer.write_zeros_exact((FEC_BLOCK_SIZE - 2 * FEC_HEADER_SIZE) as u64)?;
|
||||
writer.write_all(&header)?;
|
||||
writer
|
||||
.write_all(&self.fec)
|
||||
.map_err(|e| Error::DataWrite("fec_data", e))?;
|
||||
header
|
||||
.write_to_io(&mut writer)
|
||||
.map_err(|e| Error::DataWrite("fec_header_1", e))?;
|
||||
writer
|
||||
.write_zeros_exact((FEC_BLOCK_SIZE - 2 * header.as_bytes().len()) as u64)
|
||||
.map_err(|e| Error::DataWrite("fec_header_padding", e))?;
|
||||
header
|
||||
.write_to_io(&mut writer)
|
||||
.map_err(|e| Error::DataWrite("fec_header_2", e))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -791,7 +826,7 @@ impl<W: Write> ToWriter<W> for FecImage {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::{
|
||||
io::Seek,
|
||||
io::{Cursor, Seek},
|
||||
sync::{atomic::AtomicBool, Arc},
|
||||
};
|
||||
|
||||
@@ -851,7 +886,7 @@ mod tests {
|
||||
let mut buf = vec![0u8; size];
|
||||
rand::thread_rng().fill_bytes(&mut buf);
|
||||
file.write_all(&buf).unwrap();
|
||||
ring::digest::digest(&ring::digest::SHA256, &buf)
|
||||
aws_lc_rs::digest::digest(&aws_lc_rs::digest::SHA256, &buf)
|
||||
};
|
||||
|
||||
let fec = Fec::new(size as u64, block_size, parity).unwrap();
|
||||
@@ -886,7 +921,7 @@ mod tests {
|
||||
let mut buf = Vec::new();
|
||||
file.rewind().unwrap();
|
||||
file.read_to_end(&mut buf).unwrap();
|
||||
ring::digest::digest(&ring::digest::SHA256, &buf)
|
||||
aws_lc_rs::digest::digest(&aws_lc_rs::digest::SHA256, &buf)
|
||||
};
|
||||
assert_eq!(repaired_digest.as_ref(), orig_digest.as_ref());
|
||||
|
||||
|
||||
+143
-102
@@ -1,29 +1,31 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2023 Andrew Gunnerson
|
||||
* SPDX-License-Identifier: GPL-3.0-only
|
||||
*/
|
||||
// SPDX-FileCopyrightText: 2023-2024 Andrew Gunnerson
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use std::{
|
||||
fmt,
|
||||
io::{self, Cursor, Read, SeekFrom, Write},
|
||||
ops::Range,
|
||||
str,
|
||||
sync::atomic::AtomicBool,
|
||||
};
|
||||
|
||||
use aws_lc_rs::digest::{Algorithm, Context};
|
||||
use bstr::ByteSlice;
|
||||
use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt};
|
||||
use num_traits::ToPrimitive;
|
||||
use rayon::{
|
||||
iter::{IndexedParallelIterator, ParallelIterator},
|
||||
slice::ParallelSliceMut,
|
||||
};
|
||||
use ring::digest::{Algorithm, Context};
|
||||
use thiserror::Error;
|
||||
use zerocopy::{little_endian, FromBytes, IntoBytes};
|
||||
use zerocopy_derive::{FromBytes, Immutable, IntoBytes, KnownLayout, Unaligned};
|
||||
|
||||
use crate::{
|
||||
format::{avb, padding},
|
||||
stream::{self, FromReader, ReadSeekReopen, ReadStringExt, ToWriter, WriteStringExt},
|
||||
util::{self, NumBytes},
|
||||
format::{
|
||||
avb,
|
||||
padding::{self, ZeroPadding},
|
||||
},
|
||||
stream::{self, FromReader, ReadFixedSizeExt, ReadSeekReopen, ToWriter},
|
||||
util::{self, NumBytes, OutOfBoundsError},
|
||||
};
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
@@ -42,12 +44,20 @@ pub enum Error {
|
||||
InvalidHeaderMagic([u8; 16]),
|
||||
#[error("Invalid hash tree header version: {0}")]
|
||||
InvalidHeaderVersion(u16),
|
||||
#[error("Hashing algorithm not supported: {0:?}")]
|
||||
UnsupportedHashAlgorithm(String),
|
||||
#[error("Hashing algorithm not supported: {:?}", .0.as_bstr())]
|
||||
UnsupportedHashAlgorithm(Vec<u8>),
|
||||
#[error("{0:?} field is out of bounds")]
|
||||
FieldOutOfBounds(&'static str),
|
||||
#[error("I/O error")]
|
||||
Io(#[from] io::Error),
|
||||
IntOutOfBounds(&'static str, #[source] OutOfBoundsError),
|
||||
#[error("{0:?} overflowed integer bounds during calculations")]
|
||||
IntOverflow(&'static str),
|
||||
#[error("Failed to reopen input file")]
|
||||
InputReopen(#[source] io::Error),
|
||||
#[error("Failed to compute hash tree of input file")]
|
||||
InputDigest(#[source] io::Error),
|
||||
#[error("Failed to read hash tree data: {0}")]
|
||||
DataRead(&'static str, #[source] io::Error),
|
||||
#[error("Failed to write hash tree data: {0}")]
|
||||
DataWrite(&'static str, #[source] io::Error),
|
||||
}
|
||||
|
||||
type Result<T> = std::result::Result<T, Error>;
|
||||
@@ -83,13 +93,12 @@ impl HashTree {
|
||||
level_size = blocks
|
||||
.checked_mul(digest_size as u64)
|
||||
.and_then(|s| padding::round(s, u64::from(self.block_size)))
|
||||
.ok_or_else(|| Error::FieldOutOfBounds("level_size"))?;
|
||||
.ok_or(Error::IntOverflow("level_size"))?;
|
||||
|
||||
// Depending on the chosen block size, the original file size could
|
||||
// overflow a usize without the first level's size doing the same.
|
||||
let level_size_usize = level_size
|
||||
.to_usize()
|
||||
.ok_or_else(|| Error::FieldOutOfBounds("level_size"))?;
|
||||
let level_size_usize: usize =
|
||||
util::try_cast(level_size).map_err(|e| Error::IntOutOfBounds("level_size", e))?;
|
||||
|
||||
ranges.push(0..level_size_usize);
|
||||
}
|
||||
@@ -111,9 +120,8 @@ impl HashTree {
|
||||
fn blocks_for_ranges(&self, image_size: u64, ranges: &[Range<u64>]) -> Result<Vec<Range<u64>>> {
|
||||
let ranges = util::merge_overlapping(ranges);
|
||||
if let Some(last) = ranges.last() {
|
||||
if last.end > image_size {
|
||||
return Err(Error::FieldOutOfBounds("ranges"));
|
||||
}
|
||||
util::check_bounds(last.end, ..=image_size)
|
||||
.map_err(|e| Error::IntOutOfBounds("ranges", e))?;
|
||||
}
|
||||
|
||||
let block_size = u64::from(self.block_size);
|
||||
@@ -187,7 +195,7 @@ impl HashTree {
|
||||
cancel_signal: &AtomicBool,
|
||||
) -> io::Result<()> {
|
||||
assert!(
|
||||
size > self.block_size as u64,
|
||||
size > u64::from(self.block_size),
|
||||
"Images smaller than block size must use a normal hash",
|
||||
);
|
||||
|
||||
@@ -262,9 +270,10 @@ impl HashTree {
|
||||
) -> Result<Vec<u8>> {
|
||||
// Small files are hashed directly.
|
||||
if image_size <= u64::from(self.block_size) {
|
||||
let mut reader = input.reopen_boxed()?;
|
||||
let mut buf = vec![0u8; image_size as usize];
|
||||
reader.read_exact(&mut buf)?;
|
||||
let mut reader = input.reopen_boxed().map_err(Error::InputReopen)?;
|
||||
let buf = reader
|
||||
.read_vec_exact(image_size as usize)
|
||||
.map_err(Error::InputDigest)?;
|
||||
|
||||
let mut context = self.salted_context.clone();
|
||||
context.update(&buf);
|
||||
@@ -289,7 +298,8 @@ impl HashTree {
|
||||
prev_size as u64,
|
||||
level_data,
|
||||
cancel_signal,
|
||||
)?;
|
||||
)
|
||||
.map_err(Error::InputDigest)?;
|
||||
} else if let Some(r) = ranges {
|
||||
// Read partial blocks from file.
|
||||
let block_ranges = self.blocks_for_ranges(image_size, r)?;
|
||||
@@ -300,10 +310,12 @@ impl HashTree {
|
||||
&block_ranges,
|
||||
level_data,
|
||||
cancel_signal,
|
||||
)?;
|
||||
)
|
||||
.map_err(Error::InputDigest)?;
|
||||
} else {
|
||||
// Read entire file.
|
||||
self.hash_one_level_parallel(input, image_size, level_data, cancel_signal)?;
|
||||
self.hash_one_level_parallel(input, image_size, level_data, cancel_signal)
|
||||
.map_err(Error::InputDigest)?;
|
||||
}
|
||||
|
||||
// No need to explicitly ensure the level is padded to the block
|
||||
@@ -327,7 +339,7 @@ impl HashTree {
|
||||
cancel_signal: &AtomicBool,
|
||||
) -> Result<(Vec<u8>, Vec<u8>)> {
|
||||
let offsets = self.compute_level_offsets(image_size)?;
|
||||
let hash_tree_size = offsets.first().map(|r| r.end).unwrap_or(0);
|
||||
let hash_tree_size = offsets.first().map_or(0, |r| r.end);
|
||||
let mut hash_tree_data = vec![0u8; hash_tree_size];
|
||||
|
||||
let root_digest = self.calculate(
|
||||
@@ -353,7 +365,7 @@ impl HashTree {
|
||||
cancel_signal: &AtomicBool,
|
||||
) -> Result<Vec<u8>> {
|
||||
let offsets = self.compute_level_offsets(image_size)?;
|
||||
let hash_tree_size = offsets.first().map(|r| r.end).unwrap_or(0);
|
||||
let hash_tree_size = offsets.first().map_or(0, |r| r.end);
|
||||
if hash_tree_data.len() != hash_tree_size {
|
||||
return Err(Error::InvalidHashTreeSize {
|
||||
input: image_size,
|
||||
@@ -382,7 +394,7 @@ impl HashTree {
|
||||
cancel_signal: &AtomicBool,
|
||||
) -> Result<()> {
|
||||
let offsets = self.compute_level_offsets(image_size)?;
|
||||
let hash_tree_size = offsets.first().map(|r| r.end).unwrap_or(0);
|
||||
let hash_tree_size = offsets.first().map_or(0, |r| r.end);
|
||||
if hash_tree_data.len() != hash_tree_size {
|
||||
return Err(Error::InvalidHashTreeSize {
|
||||
input: image_size,
|
||||
@@ -404,8 +416,8 @@ impl HashTree {
|
||||
if hash_tree_data != actual_hash_tree_data {
|
||||
// These are multiple megabytes, so only report the hashes.
|
||||
let algorithm = self.salted_context.algorithm();
|
||||
let expected = ring::digest::digest(algorithm, hash_tree_data);
|
||||
let actual = ring::digest::digest(algorithm, &actual_hash_tree_data);
|
||||
let expected = aws_lc_rs::digest::digest(algorithm, hash_tree_data);
|
||||
let actual = aws_lc_rs::digest::digest(algorithm, &actual_hash_tree_data);
|
||||
|
||||
return Err(Error::InvalidHashTree {
|
||||
expected: hex::encode(expected),
|
||||
@@ -417,6 +429,28 @@ impl HashTree {
|
||||
}
|
||||
}
|
||||
|
||||
/// Raw on-disk layout for our custom hash tree image header.
|
||||
#[derive(Clone, Copy, FromBytes, IntoBytes, KnownLayout, Immutable, Unaligned)]
|
||||
#[repr(packed)]
|
||||
struct RawHeader {
|
||||
/// Magic value. This should be equal to [`HashTreeImage::MAGIC`].
|
||||
magic: [u8; 16],
|
||||
/// Image version. This should be equal to [`HashTreeImage::VERSION`].
|
||||
version: little_endian::U16,
|
||||
/// Size of the actual data.
|
||||
image_size: little_endian::U64,
|
||||
/// Block size.
|
||||
block_size: little_endian::U32,
|
||||
/// Hash algorithm.
|
||||
algorithm: [u8; 16],
|
||||
/// Salt size.
|
||||
salt_size: little_endian::U16,
|
||||
/// Root digest size.
|
||||
root_digest_size: little_endian::U16,
|
||||
/// Hash tree size.
|
||||
hash_tree_size: little_endian::U32,
|
||||
}
|
||||
|
||||
/// A type for reading and writing a custom hash tree image format.
|
||||
///
|
||||
/// File format:
|
||||
@@ -444,6 +478,7 @@ pub struct HashTreeImage {
|
||||
impl fmt::Debug for HashTreeImage {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_struct("HashTreeImage")
|
||||
.field("image_size", &self.image_size)
|
||||
.field("block_size", &self.block_size)
|
||||
.field("algorithm", &self.algorithm)
|
||||
.field("salt", &hex::encode(&self.salt))
|
||||
@@ -457,9 +492,9 @@ impl HashTreeImage {
|
||||
const MAGIC: &'static [u8; 16] = b"avbroot!hashtree";
|
||||
const VERSION: u16 = 1;
|
||||
|
||||
pub fn ring_algorithm(name: &str) -> Result<&'static Algorithm> {
|
||||
avb::ring_algorithm(name, false)
|
||||
.map_err(|_| Error::UnsupportedHashAlgorithm(name.to_owned()))
|
||||
fn digest_algorithm(name: &str) -> Result<&'static Algorithm> {
|
||||
avb::digest_algorithm(name, false)
|
||||
.map_err(|_| Error::UnsupportedHashAlgorithm(name.to_owned().into_bytes()))
|
||||
}
|
||||
|
||||
/// Generate hash tree data for a file.
|
||||
@@ -470,12 +505,12 @@ impl HashTreeImage {
|
||||
salt: &[u8],
|
||||
cancel_signal: &AtomicBool,
|
||||
) -> Result<Self> {
|
||||
let image_size = {
|
||||
let mut file = input.reopen_boxed()?;
|
||||
file.seek(SeekFrom::End(0))?
|
||||
};
|
||||
let ring_algorithm = Self::ring_algorithm(algorithm)?;
|
||||
let hash_tree = HashTree::new(block_size, ring_algorithm, salt);
|
||||
let image_size = input
|
||||
.reopen_boxed()
|
||||
.and_then(|mut f| f.seek(SeekFrom::End(0)))
|
||||
.map_err(Error::InputReopen)?;
|
||||
let digest_algorithm = Self::digest_algorithm(algorithm)?;
|
||||
let hash_tree = HashTree::new(block_size, digest_algorithm, salt);
|
||||
let (root_digest, hash_tree_data) = hash_tree.generate(input, image_size, cancel_signal)?;
|
||||
|
||||
Ok(Self {
|
||||
@@ -495,8 +530,8 @@ impl HashTreeImage {
|
||||
ranges: &[Range<u64>],
|
||||
cancel_signal: &AtomicBool,
|
||||
) -> Result<()> {
|
||||
let ring_algorithm = Self::ring_algorithm(&self.algorithm)?;
|
||||
let hash_tree = HashTree::new(self.block_size, ring_algorithm, &self.salt);
|
||||
let digest_algorithm = Self::digest_algorithm(&self.algorithm)?;
|
||||
let hash_tree = HashTree::new(self.block_size, digest_algorithm, &self.salt);
|
||||
|
||||
self.root_digest = hash_tree.update(
|
||||
input,
|
||||
@@ -515,8 +550,8 @@ impl HashTreeImage {
|
||||
input: &(dyn ReadSeekReopen + Sync),
|
||||
cancel_signal: &AtomicBool,
|
||||
) -> Result<()> {
|
||||
let ring_algorithm = Self::ring_algorithm(&self.algorithm)?;
|
||||
let hash_tree = HashTree::new(self.block_size, ring_algorithm, &self.salt);
|
||||
let digest_algorithm = Self::digest_algorithm(&self.algorithm)?;
|
||||
let hash_tree = HashTree::new(self.block_size, digest_algorithm, &self.salt);
|
||||
|
||||
hash_tree.verify(
|
||||
input,
|
||||
@@ -532,40 +567,37 @@ impl<R: Read> FromReader<R> for HashTreeImage {
|
||||
type Error = Error;
|
||||
|
||||
fn from_reader(mut reader: R) -> Result<Self> {
|
||||
let mut magic = [0u8; 16];
|
||||
reader.read_exact(&mut magic)?;
|
||||
if magic != *Self::MAGIC {
|
||||
return Err(Error::InvalidHeaderMagic(magic));
|
||||
let header =
|
||||
RawHeader::read_from_io(&mut reader).map_err(|e| Error::DataRead("header", e))?;
|
||||
|
||||
if header.magic != *Self::MAGIC {
|
||||
return Err(Error::InvalidHeaderMagic(header.magic));
|
||||
}
|
||||
|
||||
let version = reader.read_u16::<LittleEndian>()?;
|
||||
if version != Self::VERSION {
|
||||
return Err(Error::InvalidHeaderVersion(version));
|
||||
if header.version != Self::VERSION {
|
||||
return Err(Error::InvalidHeaderVersion(header.version.get()));
|
||||
}
|
||||
|
||||
let image_size = reader.read_u64::<LittleEndian>()?;
|
||||
let block_size = reader.read_u32::<LittleEndian>()?;
|
||||
let algorithm = reader.read_string_padded(16)?;
|
||||
let salt_size = reader.read_u16::<LittleEndian>()?;
|
||||
let root_digest_size = reader.read_u16::<LittleEndian>()?;
|
||||
let hash_tree_size = reader
|
||||
.read_u32::<LittleEndian>()?
|
||||
.to_usize()
|
||||
.ok_or_else(|| Error::FieldOutOfBounds("hash_tree_size"))?;
|
||||
let algorithm = header.algorithm.trim_end_padding();
|
||||
let algorithm = str::from_utf8(algorithm)
|
||||
.map_err(|_| Error::UnsupportedHashAlgorithm(algorithm.to_vec()))?;
|
||||
|
||||
let mut salt = vec![0u8; usize::from(salt_size)];
|
||||
reader.read_exact(&mut salt)?;
|
||||
let salt = reader
|
||||
.read_vec_exact(usize::from(header.salt_size))
|
||||
.map_err(|e| Error::DataRead("header", e))?;
|
||||
|
||||
let mut root_digest = vec![0u8; usize::from(root_digest_size)];
|
||||
reader.read_exact(&mut root_digest)?;
|
||||
let root_digest = reader
|
||||
.read_vec_exact(usize::from(header.root_digest_size))
|
||||
.map_err(|e| Error::DataRead("root_digest", e))?;
|
||||
|
||||
let mut hash_tree = vec![0u8; hash_tree_size];
|
||||
reader.read_exact(&mut hash_tree)?;
|
||||
let hash_tree = reader
|
||||
.read_vec_exact(header.hash_tree_size.get() as usize)
|
||||
.map_err(|e| Error::DataRead("hash_tree", e))?;
|
||||
|
||||
Ok(Self {
|
||||
image_size,
|
||||
block_size,
|
||||
algorithm,
|
||||
image_size: header.image_size.get(),
|
||||
block_size: header.block_size.get(),
|
||||
algorithm: algorithm.to_owned(),
|
||||
salt,
|
||||
root_digest,
|
||||
hash_tree,
|
||||
@@ -577,33 +609,42 @@ impl<W: Write> ToWriter<W> for HashTreeImage {
|
||||
type Error = Error;
|
||||
|
||||
fn to_writer(&self, mut writer: W) -> Result<()> {
|
||||
let salt_size = self
|
||||
.salt
|
||||
.len()
|
||||
.to_u16()
|
||||
.ok_or_else(|| Error::FieldOutOfBounds("salt_size"))?;
|
||||
let root_digest_size = self
|
||||
.root_digest
|
||||
.len()
|
||||
.to_u16()
|
||||
.ok_or_else(|| Error::FieldOutOfBounds("root_digest_size"))?;
|
||||
let hash_tree_size = self
|
||||
.hash_tree
|
||||
.len()
|
||||
.to_u32()
|
||||
.ok_or_else(|| Error::FieldOutOfBounds("hash_tree_size"))?;
|
||||
let algorithm = self
|
||||
.algorithm
|
||||
.as_bytes()
|
||||
.to_padded_array::<16>()
|
||||
.ok_or_else(|| Error::UnsupportedHashAlgorithm(self.algorithm.as_bytes().to_vec()))?;
|
||||
|
||||
writer.write_all(Self::MAGIC)?;
|
||||
writer.write_u16::<LittleEndian>(Self::VERSION)?;
|
||||
writer.write_u64::<LittleEndian>(self.image_size)?;
|
||||
writer.write_u32::<LittleEndian>(self.block_size)?;
|
||||
writer.write_string_padded(&self.algorithm, 16)?;
|
||||
writer.write_u16::<LittleEndian>(salt_size)?;
|
||||
writer.write_u16::<LittleEndian>(root_digest_size)?;
|
||||
writer.write_u32::<LittleEndian>(hash_tree_size)?;
|
||||
writer.write_all(&self.salt)?;
|
||||
writer.write_all(&self.root_digest)?;
|
||||
writer.write_all(&self.hash_tree)?;
|
||||
let salt_size: u16 =
|
||||
util::try_cast(self.salt.len()).map_err(|e| Error::IntOutOfBounds("salt_size", e))?;
|
||||
let root_digest_size: u16 = util::try_cast(self.root_digest.len())
|
||||
.map_err(|e| Error::IntOutOfBounds("root_digest_size", e))?;
|
||||
let hash_tree_size: u32 = util::try_cast(self.hash_tree.len())
|
||||
.map_err(|e| Error::IntOutOfBounds("hash_tree_size", e))?;
|
||||
|
||||
let header = RawHeader {
|
||||
magic: *Self::MAGIC,
|
||||
version: Self::VERSION.into(),
|
||||
image_size: self.image_size.into(),
|
||||
block_size: self.block_size.into(),
|
||||
algorithm,
|
||||
salt_size: salt_size.into(),
|
||||
root_digest_size: root_digest_size.into(),
|
||||
hash_tree_size: hash_tree_size.into(),
|
||||
};
|
||||
|
||||
header
|
||||
.write_to_io(&mut writer)
|
||||
.map_err(|e| Error::DataWrite("header", e))?;
|
||||
writer
|
||||
.write_all(&self.salt)
|
||||
.map_err(|e| Error::DataWrite("salt", e))?;
|
||||
writer
|
||||
.write_all(&self.root_digest)
|
||||
.map_err(|e| Error::DataWrite("root_digest", e))?;
|
||||
writer
|
||||
.write_all(&self.hash_tree)
|
||||
.map_err(|e| Error::DataWrite("hash_tree", e))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -621,7 +662,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn calculate_level_ranges() {
|
||||
let hash_tree = HashTree::new(4096, &ring::digest::SHA256, &[]);
|
||||
let hash_tree = HashTree::new(4096, &aws_lc_rs::digest::SHA256, &[]);
|
||||
assert_eq!(
|
||||
hash_tree.compute_level_offsets(0).unwrap(),
|
||||
&[] as &[Range<usize>],
|
||||
@@ -634,7 +675,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn blocks_for_ranges() {
|
||||
let hash_tree = HashTree::new(4096, &ring::digest::SHA256, b"Salt");
|
||||
let hash_tree = HashTree::new(4096, &aws_lc_rs::digest::SHA256, b"Salt");
|
||||
assert_eq!(
|
||||
hash_tree.blocks_for_ranges(16384, &[0..16384]).unwrap(),
|
||||
&[0..4],
|
||||
@@ -648,14 +689,14 @@ mod tests {
|
||||
);
|
||||
assert_matches!(
|
||||
hash_tree.blocks_for_ranges(16384, &[0..16385]),
|
||||
Err(Error::FieldOutOfBounds(_))
|
||||
Err(Error::IntOutOfBounds(_, _))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn generate_update_verify() {
|
||||
let cancel_signal = AtomicBool::new(false);
|
||||
let hash_tree = HashTree::new(64, &ring::digest::SHA256, b"Salt");
|
||||
let hash_tree = HashTree::new(64, &aws_lc_rs::digest::SHA256, b"Salt");
|
||||
let mut input = SharedCursor::new();
|
||||
|
||||
// Try input smaller than one block.
|
||||
|
||||
+414
-275
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,5 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2023 Andrew Gunnerson
|
||||
* SPDX-License-Identifier: GPL-3.0-only
|
||||
*/
|
||||
// SPDX-FileCopyrightText: 2023 Andrew Gunnerson
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
pub mod avb;
|
||||
pub mod bootimage;
|
||||
|
||||
+253
-135
@@ -1,22 +1,21 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2022-2024 Andrew Gunnerson
|
||||
* SPDX-License-Identifier: GPL-3.0-only
|
||||
*/
|
||||
// SPDX-FileCopyrightText: 2022-2025 Andrew Gunnerson
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use std::{
|
||||
collections::BTreeMap,
|
||||
fmt,
|
||||
io::{self, Cursor, Read, Seek, SeekFrom, Write},
|
||||
iter,
|
||||
path::Path,
|
||||
sync::atomic::AtomicBool,
|
||||
};
|
||||
|
||||
use aws_lc_rs::digest::Context;
|
||||
use clap::ValueEnum;
|
||||
use cms::signed_data::SignedData;
|
||||
use const_oid::{db::rfc5912, ObjectIdentifier};
|
||||
use memchr::memmem;
|
||||
use prost::Message;
|
||||
use ring::digest::Context;
|
||||
use thiserror::Error;
|
||||
use x509_cert::{der::Encode, Certificate};
|
||||
use zip::{result::ZipError, write::FileOptions, CompressionMethod, ZipArchive, ZipWriter};
|
||||
@@ -25,7 +24,7 @@ use crate::{
|
||||
crypto::{self, RsaPublicKeyExt, RsaSigningKey, SignatureAlgorithm},
|
||||
format::payload::{self, PayloadHeader},
|
||||
protobuf::build::tools::releasetools::{ota_metadata::OtaType, OtaMetadata},
|
||||
stream::{self, FromReader, HashingReader, HashingWriter},
|
||||
stream::{self, FromReader, HashingReader, HashingWriter, ReadFixedSizeExt},
|
||||
};
|
||||
|
||||
pub const PATH_METADATA: &str = "META-INF/com/android/metadata";
|
||||
@@ -51,10 +50,12 @@ pub enum Error {
|
||||
OtaMagicNotFound,
|
||||
#[error("Cannot find EOCD magic")]
|
||||
EocdMagicNotFound,
|
||||
#[error("EOCD magic found in archive comment")]
|
||||
EocdMagicInComment,
|
||||
#[error("EOCD magic found in archive comment at offset {0}")]
|
||||
EocdMagicInComment(usize),
|
||||
#[error("Zip is too small to contain EOCD")]
|
||||
ZipTooSmall,
|
||||
#[error("Zip archive comment is not empty: {0}")]
|
||||
ZipNonEmptyComment(u16),
|
||||
#[error("Signature offset exceeds archive comment size")]
|
||||
SignatureOffsetTooLarge,
|
||||
#[error("Expected exactly one CMS embedded certificate, but found {0}")]
|
||||
@@ -71,32 +72,50 @@ pub enum Error {
|
||||
UnsupportedLegacyMetadataField { key: String, value: String },
|
||||
#[error("Expected entry offsets {expected:?}, but have {actual:?}")]
|
||||
MismatchedPropertyFiles { expected: String, actual: String },
|
||||
#[error("Property files {0:?} exceed {1} byte reserved space")]
|
||||
InsufficientReservedSpace(String, usize),
|
||||
#[error("Property files {value:?} exceed {reserved} byte reserved space")]
|
||||
InsufficientReservedSpace { value: String, reserved: usize },
|
||||
#[error("Invalid property file entry: {0:?}")]
|
||||
InvalidPropertyFileEntry(String),
|
||||
#[error("Missing entry in OTA zip: {0}")]
|
||||
#[error("Missing entry in OTA zip: {0:?}")]
|
||||
MissingZipEntry(&'static str),
|
||||
#[error("CMS signing error")]
|
||||
CmsSign(#[from] crypto::Error),
|
||||
#[error("Payload error")]
|
||||
Payload(#[from] payload::Error),
|
||||
#[error("Failed to decode protobuf message")]
|
||||
ProtobufDecode(#[from] prost::DecodeError),
|
||||
#[error("SPKI error")]
|
||||
Spki(#[from] pkcs8::spki::Error),
|
||||
#[error("x509 DER error")]
|
||||
Der(#[from] x509_cert::der::Error),
|
||||
#[error("Zip error")]
|
||||
Zip(#[from] ZipError),
|
||||
#[error("I/O error")]
|
||||
Io(#[from] io::Error),
|
||||
#[error("Failed to decode OTA metadata protobuf message")]
|
||||
MetadataDecode(#[source] prost::DecodeError),
|
||||
#[error("Failed to open zip file")]
|
||||
ZipOpen(#[source] ZipError),
|
||||
#[error("Failed to open zip entry: {0:?}")]
|
||||
ZipEntryOpen(&'static str, #[source] ZipError),
|
||||
#[error("Failed to start new zip entry: {0:?}")]
|
||||
ZipEntryStart(&'static str, #[source] ZipError),
|
||||
#[error("Failed to read zip entry: {0:?}")]
|
||||
ZipEntryRead(&'static str, #[source] io::Error),
|
||||
#[error("Failed to write zip entry: {0:?}")]
|
||||
ZipEntryWrite(&'static str, #[source] io::Error),
|
||||
#[error("Failed to open zip entry #{0}")]
|
||||
ZipIndexOpen(usize, #[source] ZipError),
|
||||
#[error("Failed to load OTA certificate")]
|
||||
OtaCertLoad(#[source] crypto::Error),
|
||||
#[error("Failed to extract public key from OTA certificate")]
|
||||
OtaCertExtractPubKey(#[source] crypto::Error),
|
||||
#[error("Failed to load payload binary")]
|
||||
PayloadLoad(#[source] payload::Error),
|
||||
#[error("Failed to load CMS signature")]
|
||||
CmsLoad(#[source] crypto::Error),
|
||||
#[error("Failed to save CMS signature")]
|
||||
CmsSave(#[source] x509_cert::der::Error),
|
||||
#[error("Failed to generate CMS signature")]
|
||||
CmsSign(#[source] crypto::Error),
|
||||
#[error("Failed to verify CMS signature")]
|
||||
CmsVerify(#[source] crypto::Error),
|
||||
#[error("Failed to read OTA data: {0}")]
|
||||
DataRead(&'static str, #[source] io::Error),
|
||||
#[error("Failed to write OTA data: {0}")]
|
||||
DataWrite(&'static str, #[source] io::Error),
|
||||
}
|
||||
|
||||
type Result<T> = std::result::Result<T, Error>;
|
||||
|
||||
pub fn parse_protobuf_metadata(data: &[u8]) -> Result<OtaMetadata> {
|
||||
Ok(OtaMetadata::decode(data)?)
|
||||
OtaMetadata::decode(data).map_err(Error::MetadataDecode)
|
||||
}
|
||||
|
||||
/// Synthesize protobuf structure from legacy plain-text metadata.
|
||||
@@ -137,7 +156,7 @@ pub fn parse_legacy_metadata(data: &str) -> Result<OtaMetadata> {
|
||||
}
|
||||
"ota-wipe" => metadata.wipe = parse_yes()?,
|
||||
"ota-retrofit-dynamic-partitions" => {
|
||||
metadata.retrofit_dynamic_partitions = parse_yes()?
|
||||
metadata.retrofit_dynamic_partitions = parse_yes()?;
|
||||
}
|
||||
"ota-downgrade" => metadata.downgrade = parse_yes()?,
|
||||
"ota-required-cache" => {
|
||||
@@ -192,7 +211,7 @@ pub fn parse_legacy_metadata(data: &str) -> Result<OtaMetadata> {
|
||||
|
||||
/// Generate the legacy plain-text and modern protobuf serializations of the
|
||||
/// given metadata instance.
|
||||
fn serialize_metadata(metadata: &OtaMetadata) -> Result<(String, Vec<u8>)> {
|
||||
fn serialize_metadata(metadata: &OtaMetadata) -> (String, Vec<u8>) {
|
||||
use std::fmt::Write;
|
||||
|
||||
let mut pairs = BTreeMap::<String, String>::new();
|
||||
@@ -256,7 +275,7 @@ fn serialize_metadata(metadata: &OtaMetadata) -> Result<(String, Vec<u8>)> {
|
||||
});
|
||||
let modern_metadata = metadata.encode_to_vec();
|
||||
|
||||
Ok((legacy_metadata, modern_metadata))
|
||||
(legacy_metadata, modern_metadata)
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
@@ -302,12 +321,13 @@ fn compute_property_files(
|
||||
pf_name: &str,
|
||||
entries: &[ZipEntry],
|
||||
max_length: Option<usize>,
|
||||
want_pb: bool,
|
||||
) -> Result<String> {
|
||||
let compute = |path: &'static str| -> Result<String> {
|
||||
let entry = entries
|
||||
.iter()
|
||||
.find(|e| e.name == path)
|
||||
.ok_or_else(|| Error::MissingZipEntry(path))?;
|
||||
.ok_or(Error::MissingZipEntry(path))?;
|
||||
let name = path.rsplit_once('/').map_or(path, |p| p.1);
|
||||
|
||||
Ok(format!("{name}:{}:{}", entry.offset, entry.size))
|
||||
@@ -336,17 +356,24 @@ fn compute_property_files(
|
||||
|
||||
if max_length.is_none() {
|
||||
tokens.push(format!("metadata:{}", " ".repeat(15)));
|
||||
tokens.push(format!("metadata.pb:{}", " ".repeat(15)));
|
||||
if want_pb {
|
||||
tokens.push(format!("metadata.pb:{}", " ".repeat(15)));
|
||||
}
|
||||
} else {
|
||||
tokens.push(compute(PATH_METADATA)?);
|
||||
tokens.push(compute(PATH_METADATA_PB)?);
|
||||
if want_pb {
|
||||
tokens.push(compute(PATH_METADATA_PB)?);
|
||||
}
|
||||
}
|
||||
|
||||
let mut joined = tokens.join(",");
|
||||
|
||||
if let Some(l) = max_length {
|
||||
if joined.len() > l {
|
||||
return Err(Error::InsufficientReservedSpace(joined, l));
|
||||
return Err(Error::InsufficientReservedSpace {
|
||||
value: joined,
|
||||
reserved: l,
|
||||
});
|
||||
}
|
||||
|
||||
let remain = l - joined.len();
|
||||
@@ -365,7 +392,7 @@ fn add_payload_metadata_entry(
|
||||
let payload_offset = entries
|
||||
.iter()
|
||||
.find(|e| e.name == PATH_PAYLOAD)
|
||||
.ok_or_else(|| Error::MissingZipEntry(PATH_PAYLOAD))?
|
||||
.ok_or(Error::MissingZipEntry(PATH_PAYLOAD))?
|
||||
.offset;
|
||||
entries.push(ZipEntry {
|
||||
name: NAME_PAYLOAD_METADATA.to_owned(),
|
||||
@@ -417,26 +444,38 @@ pub fn add_metadata(
|
||||
for pf in [PF_NAME, PF_STREAMING_NAME] {
|
||||
metadata.property_files.insert(
|
||||
pf.to_owned(),
|
||||
compute_property_files(pf, &zip_entries, None)?,
|
||||
compute_property_files(pf, &zip_entries, None, true)?,
|
||||
);
|
||||
}
|
||||
|
||||
// Add the placeholders to a temporary zip to compute final property files.
|
||||
let (temp_legacy_offset, temp_modern_offset) = {
|
||||
let (legacy_raw, modern_raw) = serialize_metadata(&metadata)?;
|
||||
let (legacy_raw, modern_raw) = serialize_metadata(&metadata);
|
||||
let raw_writer = Cursor::new(Vec::new());
|
||||
let mut writer = match zip_mode {
|
||||
ZipMode::Streaming => ZipWriter::new_streaming(raw_writer),
|
||||
ZipMode::Seekable => ZipWriter::new(raw_writer),
|
||||
};
|
||||
|
||||
writer.start_file_with_extra_data(PATH_METADATA, options)?;
|
||||
let legacy_offset = writer.end_extra_data()?;
|
||||
writer.write_all(legacy_raw.as_bytes())?;
|
||||
writer
|
||||
.start_file_with_extra_data(PATH_METADATA, options)
|
||||
.map_err(|e| Error::ZipEntryStart(PATH_METADATA, e))?;
|
||||
let legacy_offset = writer
|
||||
.end_extra_data()
|
||||
.map_err(|e| Error::ZipEntryStart(PATH_METADATA, e))?;
|
||||
writer
|
||||
.write_all(legacy_raw.as_bytes())
|
||||
.map_err(|e| Error::ZipEntryWrite(PATH_METADATA, e))?;
|
||||
|
||||
writer.start_file_with_extra_data(PATH_METADATA_PB, options)?;
|
||||
let modern_offset = writer.end_extra_data()?;
|
||||
writer.write_all(&modern_raw)?;
|
||||
writer
|
||||
.start_file_with_extra_data(PATH_METADATA_PB, options)
|
||||
.map_err(|e| Error::ZipEntryStart(PATH_METADATA_PB, e))?;
|
||||
let modern_offset = writer
|
||||
.end_extra_data()
|
||||
.map_err(|e| Error::ZipEntryStart(PATH_METADATA_PB, e))?;
|
||||
writer
|
||||
.write_all(&modern_raw)
|
||||
.map_err(|e| Error::ZipEntryWrite(PATH_METADATA_PB, e))?;
|
||||
|
||||
zip_entries.push(ZipEntry {
|
||||
name: PATH_METADATA.to_owned(),
|
||||
@@ -454,20 +493,32 @@ pub fn add_metadata(
|
||||
|
||||
// Compute the final property files using the offsets of the fake entries.
|
||||
for (key, value) in &mut metadata.property_files {
|
||||
*value = compute_property_files(key, &zip_entries, Some(value.len()))?;
|
||||
*value = compute_property_files(key, &zip_entries, Some(value.len()), true)?;
|
||||
}
|
||||
|
||||
// Add the final metadata files to the real zip.
|
||||
{
|
||||
let (legacy_raw, modern_raw) = serialize_metadata(&metadata)?;
|
||||
let (legacy_raw, modern_raw) = serialize_metadata(&metadata);
|
||||
|
||||
zip_writer.start_file_with_extra_data(PATH_METADATA, options)?;
|
||||
let legacy_offset = zip_writer.end_extra_data()?;
|
||||
zip_writer.write_all(legacy_raw.as_bytes())?;
|
||||
zip_writer
|
||||
.start_file_with_extra_data(PATH_METADATA, options)
|
||||
.map_err(|e| Error::ZipEntryStart(PATH_METADATA, e))?;
|
||||
let legacy_offset = zip_writer
|
||||
.end_extra_data()
|
||||
.map_err(|e| Error::ZipEntryStart(PATH_METADATA, e))?;
|
||||
zip_writer
|
||||
.write_all(legacy_raw.as_bytes())
|
||||
.map_err(|e| Error::ZipEntryWrite(PATH_METADATA, e))?;
|
||||
|
||||
zip_writer.start_file_with_extra_data(PATH_METADATA_PB, options)?;
|
||||
let modern_offset = zip_writer.end_extra_data()?;
|
||||
zip_writer.write_all(&modern_raw)?;
|
||||
zip_writer
|
||||
.start_file_with_extra_data(PATH_METADATA_PB, options)
|
||||
.map_err(|e| Error::ZipEntryStart(PATH_METADATA_PB, e))?;
|
||||
let modern_offset = zip_writer
|
||||
.end_extra_data()
|
||||
.map_err(|e| Error::ZipEntryStart(PATH_METADATA_PB, e))?;
|
||||
zip_writer
|
||||
.write_all(&modern_raw)
|
||||
.map_err(|e| Error::ZipEntryWrite(PATH_METADATA_PB, e))?;
|
||||
|
||||
assert_eq!(legacy_offset, temp_legacy_offset);
|
||||
assert_eq!(modern_offset, temp_modern_offset);
|
||||
@@ -482,11 +533,13 @@ pub fn verify_metadata(
|
||||
metadata: &OtaMetadata,
|
||||
payload_metadata_size: u64,
|
||||
) -> Result<()> {
|
||||
let mut zip_reader = ZipArchive::new(reader)?;
|
||||
let mut zip_reader = ZipArchive::new(reader).map_err(Error::ZipOpen)?;
|
||||
let mut zip_entries = vec![];
|
||||
|
||||
for i in 0..zip_reader.len() {
|
||||
let entry = zip_reader.by_index(i)?;
|
||||
let entry = zip_reader
|
||||
.by_index(i)
|
||||
.map_err(|e| Error::ZipIndexOpen(i, e))?;
|
||||
zip_entries.push(ZipEntry {
|
||||
name: entry.name().to_owned(),
|
||||
offset: entry.data_start(),
|
||||
@@ -496,8 +549,11 @@ pub fn verify_metadata(
|
||||
|
||||
add_payload_metadata_entry(&mut zip_entries, payload_metadata_size)?;
|
||||
|
||||
let metadata_pb = zip_entries.iter().find(|e| e.name == PATH_METADATA_PB);
|
||||
|
||||
for (key, value) in &metadata.property_files {
|
||||
let new_value = compute_property_files(key, &zip_entries, Some(value.len()))?;
|
||||
let new_value =
|
||||
compute_property_files(key, &zip_entries, Some(value.len()), metadata_pb.is_some())?;
|
||||
if *value != new_value {
|
||||
return Err(Error::MismatchedPropertyFiles {
|
||||
expected: value.clone(),
|
||||
@@ -509,16 +565,43 @@ pub fn verify_metadata(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Parse the CMS signature from the OTA zip comment. Returns the decoded CMS
|
||||
/// [`SignedData`] structure and the length of the file (from the beginning)
|
||||
/// that's covered by the signature. This does not perform any parsing of zip
|
||||
/// data structures.
|
||||
fn parse_ota_sig(mut reader: impl Read + Seek) -> Result<(SignedData, u64)> {
|
||||
let file_size = reader.seek(SeekFrom::End(0))?;
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct OtaSignature {
|
||||
/// Decoded CMS structure.
|
||||
pub signed_data: SignedData,
|
||||
/// Length of the file (from the beginning) that's covered by the signature.
|
||||
pub hashed_size: u64,
|
||||
}
|
||||
|
||||
reader.seek(SeekFrom::Current(-6))?;
|
||||
let mut footer = [0u8; 6];
|
||||
reader.read_exact(&mut footer)?;
|
||||
impl OtaSignature {
|
||||
pub fn embedded_cert(&self) -> Result<&Certificate> {
|
||||
let mut iter = crypto::iter_cms_certs(&self.signed_data);
|
||||
|
||||
let Some(cert) = iter.next() else {
|
||||
return Err(Error::NotOneCmsCertificate(0));
|
||||
};
|
||||
|
||||
let None = iter.next() else {
|
||||
return Err(Error::NotOneCmsCertificate(2 + iter.count()));
|
||||
};
|
||||
|
||||
Ok(cert)
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse the CMS signature from the OTA zip comment. This does not perform any
|
||||
/// parsing of zip data structures.
|
||||
pub fn parse_ota_sig(mut reader: impl Read + Seek) -> Result<OtaSignature> {
|
||||
let file_size = reader
|
||||
.seek(SeekFrom::End(0))
|
||||
.map_err(|e| Error::DataRead("file_size", e))?;
|
||||
|
||||
reader
|
||||
.seek(SeekFrom::Current(-6))
|
||||
.map_err(|e| Error::DataRead("footer", e))?;
|
||||
let footer = reader
|
||||
.read_array_exact::<6>()
|
||||
.map_err(|e| Error::DataRead("footer", e))?;
|
||||
|
||||
let abs_eoc_offset = u16::from_le_bytes(footer[0..2].try_into().unwrap());
|
||||
let sig_magic = u16::from_le_bytes(footer[2..4].try_into().unwrap());
|
||||
@@ -537,25 +620,32 @@ fn parse_ota_sig(mut reader: impl Read + Seek) -> Result<(SignedData, u64)> {
|
||||
return Err(Error::SignatureOffsetTooLarge);
|
||||
}
|
||||
|
||||
reader.seek(SeekFrom::Start(file_size - eocd_size))?;
|
||||
let mut eocd = vec![0u8; eocd_size as usize];
|
||||
reader.read_exact(&mut eocd)?;
|
||||
reader
|
||||
.seek(SeekFrom::Start(file_size - eocd_size))
|
||||
.map_err(|e| Error::DataRead("eocd", e))?;
|
||||
let eocd = reader
|
||||
.read_vec_exact(eocd_size as usize)
|
||||
.map_err(|e| Error::DataRead("eocd", e))?;
|
||||
|
||||
let mut eocd_magic_iter = memmem::find_iter(&eocd, ZIP_EOCD_MAGIC);
|
||||
if eocd_magic_iter.next() != Some(0) {
|
||||
return Err(Error::EocdMagicNotFound);
|
||||
}
|
||||
if eocd_magic_iter.next().is_some() {
|
||||
return Err(Error::EocdMagicInComment);
|
||||
if let Some(offset) = eocd_magic_iter.next() {
|
||||
return Err(Error::EocdMagicInComment(offset));
|
||||
}
|
||||
|
||||
let sig_offset = eocd_size as usize - usize::from(abs_eoc_offset);
|
||||
let sd = crypto::parse_cms(&eocd[sig_offset..eocd_size as usize - 6])?;
|
||||
let signed_data =
|
||||
crypto::parse_cms(&eocd[sig_offset..eocd_size as usize - 6]).map_err(Error::CmsLoad)?;
|
||||
// The signature covers everything aside from the archive comment and its
|
||||
// length field.
|
||||
let hashed_size = file_size - 2 - u64::from(comment_size);
|
||||
|
||||
Ok((sd, hashed_size))
|
||||
Ok(OtaSignature {
|
||||
signed_data,
|
||||
hashed_size,
|
||||
})
|
||||
}
|
||||
|
||||
/// Verify an OTA zip against its embedded certificates. This function makes no
|
||||
@@ -566,26 +656,19 @@ fn parse_ota_sig(mut reader: impl Read + Seek) -> Result<(SignedData, u64)> {
|
||||
/// does not support them either. It expects the CMS [`SignedData`] structure to
|
||||
/// be used for nothing more than a raw signature transport mechanism.
|
||||
pub fn verify_ota(mut reader: impl Read + Seek, cancel_signal: &AtomicBool) -> Result<Certificate> {
|
||||
let (sd, hashed_size) = parse_ota_sig(&mut reader)?;
|
||||
|
||||
// Make sure the certificate in the CMS structure matches the otacert zip
|
||||
// entry.
|
||||
let certs = crypto::get_cms_certs(&sd);
|
||||
if certs.len() != 1 {
|
||||
return Err(Error::NotOneCmsCertificate(certs.len()));
|
||||
}
|
||||
|
||||
let cert = &certs[0];
|
||||
let public_key = crypto::get_public_key(cert)?;
|
||||
let ota_sig = parse_ota_sig(&mut reader)?;
|
||||
let cert = ota_sig.embedded_cert()?;
|
||||
let public_key = crypto::get_public_key(cert).map_err(Error::OtaCertExtractPubKey)?;
|
||||
|
||||
// Make sure this is a signature scheme we can handle. There's currently no
|
||||
// Rust library to verify arbitrary CMS signatures for large files without
|
||||
// fully reading them into memory.
|
||||
if sd.signer_infos.0.len() != 1 {
|
||||
return Err(Error::NotOneCmsSignerInfo(sd.signer_infos.0.len()));
|
||||
let signers_len = ota_sig.signed_data.signer_infos.0.len();
|
||||
if signers_len != 1 {
|
||||
return Err(Error::NotOneCmsSignerInfo(signers_len));
|
||||
}
|
||||
|
||||
let signer = sd.signer_infos.0.get(0).unwrap();
|
||||
let signer = ota_sig.signed_data.signer_infos.0.get(0).unwrap();
|
||||
if signer.digest_alg.oid != rfc5912::ID_SHA_256 && signer.digest_alg.oid != rfc5912::ID_SHA_1 {
|
||||
return Err(Error::UnsupportedDigestAlgorithm(signer.digest_alg.oid));
|
||||
} else if signer.signature_algorithm.oid != rfc5912::RSA_ENCRYPTION
|
||||
@@ -597,27 +680,40 @@ pub fn verify_ota(mut reader: impl Read + Seek, cancel_signal: &AtomicBool) -> R
|
||||
}
|
||||
|
||||
// Manually hash the parts of the file covered by the signature.
|
||||
reader.seek(SeekFrom::Start(0))?;
|
||||
reader
|
||||
.seek(SeekFrom::Start(0))
|
||||
.map_err(|e| Error::DataRead("raw_data", e))?;
|
||||
|
||||
// We support SHA1 for verification only.
|
||||
let (algorithm, algo) = if signer.digest_alg.oid == rfc5912::ID_SHA_256 {
|
||||
(&ring::digest::SHA256, SignatureAlgorithm::Sha256WithRsa)
|
||||
(
|
||||
&aws_lc_rs::digest::SHA256,
|
||||
SignatureAlgorithm::Sha256WithRsa,
|
||||
)
|
||||
} else {
|
||||
(
|
||||
&ring::digest::SHA1_FOR_LEGACY_USE_ONLY,
|
||||
&aws_lc_rs::digest::SHA1_FOR_LEGACY_USE_ONLY,
|
||||
SignatureAlgorithm::Sha1WithRsa,
|
||||
)
|
||||
};
|
||||
|
||||
let mut hashing_reader = HashingReader::new(reader, Context::new(algorithm));
|
||||
|
||||
stream::copy_n(&mut hashing_reader, io::sink(), hashed_size, cancel_signal)?;
|
||||
stream::copy_n(
|
||||
&mut hashing_reader,
|
||||
io::sink(),
|
||||
ota_sig.hashed_size,
|
||||
cancel_signal,
|
||||
)
|
||||
.map_err(|e| Error::DataRead("raw_data", e))?;
|
||||
|
||||
let (_, context) = hashing_reader.finish();
|
||||
let digest = context.finish();
|
||||
|
||||
// Verify the signature against the public key.
|
||||
public_key.verify_sig(algo, digest.as_ref(), signer.signature.as_bytes())?;
|
||||
public_key
|
||||
.verify_sig(algo, digest.as_ref(), signer.signature.as_bytes())
|
||||
.map_err(Error::CmsVerify)?;
|
||||
|
||||
Ok(cert.clone())
|
||||
}
|
||||
@@ -627,29 +723,52 @@ pub fn verify_ota(mut reader: impl Read + Seek, cancel_signal: &AtomicBool) -> R
|
||||
pub fn parse_zip_ota_info(
|
||||
reader: impl Read + Seek,
|
||||
) -> Result<(OtaMetadata, Certificate, PayloadHeader, String)> {
|
||||
let mut zip = ZipArchive::new(reader)?;
|
||||
let mut zip = ZipArchive::new(reader).map_err(Error::ZipOpen)?;
|
||||
|
||||
let metadata = {
|
||||
let mut entry = zip.by_name(PATH_METADATA_PB)?;
|
||||
let mut buf = Vec::new();
|
||||
entry.read_to_end(&mut buf)?;
|
||||
OtaMetadata::decode(buf.as_slice())?
|
||||
let metadata = match zip.by_name(PATH_METADATA_PB) {
|
||||
Ok(mut entry) => {
|
||||
let mut buf = Vec::new();
|
||||
entry
|
||||
.read_to_end(&mut buf)
|
||||
.map_err(|e| Error::ZipEntryRead(PATH_METADATA_PB, e))?;
|
||||
parse_protobuf_metadata(&buf)?
|
||||
}
|
||||
e @ Err(ZipError::FileNotFound) => {
|
||||
drop(e);
|
||||
let mut entry = zip
|
||||
.by_name(PATH_METADATA)
|
||||
.map_err(|e| Error::ZipEntryOpen(PATH_METADATA, e))?;
|
||||
let mut buf = String::new();
|
||||
entry
|
||||
.read_to_string(&mut buf)
|
||||
.map_err(|e| Error::ZipEntryRead(PATH_METADATA, e))?;
|
||||
parse_legacy_metadata(&buf)?
|
||||
}
|
||||
Err(e) => return Err(Error::ZipEntryOpen(PATH_METADATA_PB, e)),
|
||||
};
|
||||
|
||||
let certificate = {
|
||||
let entry = zip.by_name(PATH_OTACERT)?;
|
||||
crypto::read_pem_cert(entry)?
|
||||
let entry = zip
|
||||
.by_name(PATH_OTACERT)
|
||||
.map_err(|e| Error::ZipEntryOpen(PATH_OTACERT, e))?;
|
||||
crypto::read_pem_cert(Path::new(PATH_OTACERT), entry).map_err(Error::OtaCertLoad)?
|
||||
};
|
||||
|
||||
let header = {
|
||||
let entry = zip.by_name(PATH_PAYLOAD)?;
|
||||
PayloadHeader::from_reader(entry)?
|
||||
let entry = zip
|
||||
.by_name(PATH_PAYLOAD)
|
||||
.map_err(|e| Error::ZipEntryOpen(PATH_PAYLOAD, e))?;
|
||||
PayloadHeader::from_reader(entry).map_err(Error::PayloadLoad)?
|
||||
};
|
||||
|
||||
let properties = {
|
||||
let mut entry = zip.by_name(PATH_PROPERTIES)?;
|
||||
let mut entry = zip
|
||||
.by_name(PATH_PROPERTIES)
|
||||
.map_err(|e| Error::ZipEntryOpen(PATH_PROPERTIES, e))?;
|
||||
let mut buf = String::new();
|
||||
entry.read_to_string(&mut buf)?;
|
||||
entry
|
||||
.read_to_string(&mut buf)
|
||||
.map_err(|e| Error::ZipEntryRead(PATH_PROPERTIES, e))?;
|
||||
buf
|
||||
};
|
||||
|
||||
@@ -657,17 +776,12 @@ pub fn parse_zip_ota_info(
|
||||
}
|
||||
|
||||
/// Ensure that we're using a non-zip64 EOCD and there's no archive comment.
|
||||
fn validate_eocd(eocd: &[u8]) -> io::Result<()> {
|
||||
fn validate_eocd(eocd: &[u8]) -> Result<()> {
|
||||
if &eocd[..4] != b"PK\x05\x06" {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidData,
|
||||
"EOCD magic not found",
|
||||
));
|
||||
return Err(Error::EocdMagicNotFound);
|
||||
} else if &eocd[20..22] != b"\0\0" {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidData,
|
||||
"Archive comment is not 0 bytes",
|
||||
));
|
||||
let size = u16::from_le_bytes(eocd[20..22].try_into().unwrap());
|
||||
return Err(Error::ZipNonEmptyComment(size));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@@ -680,10 +794,11 @@ fn validate_eocd(eocd: &[u8]) -> io::Result<()> {
|
||||
fn compute_signature_comment(
|
||||
key: &RsaSigningKey,
|
||||
cert: &Certificate,
|
||||
digest: ring::digest::Digest,
|
||||
digest: aws_lc_rs::digest::Digest,
|
||||
) -> Result<Vec<u8>> {
|
||||
let cms_signature = crypto::cms_sign_external(key, cert, digest.as_ref())?;
|
||||
let cms_signature_der = cms_signature.to_der()?;
|
||||
let cms_signature =
|
||||
crypto::cms_sign_external(key, cert, digest.as_ref()).map_err(Error::CmsSign)?;
|
||||
let cms_signature_der = cms_signature.to_der().map_err(Error::CmsSave)?;
|
||||
|
||||
// Includes placeholder for the EOCD comment size field.
|
||||
let mut buf = vec![0; 2];
|
||||
@@ -705,12 +820,8 @@ fn compute_signature_comment(
|
||||
// Archive comment size (for use by the OTA signature verifier).
|
||||
buf.extend(((comment_size) as u16).to_le_bytes());
|
||||
|
||||
if let Some(o) = memmem::find(&buf[2..], ZIP_EOCD_MAGIC) {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidData,
|
||||
format!("Archive comment contains EOCD magic at offset {o}"),
|
||||
)
|
||||
.into());
|
||||
if let Some(offset) = memmem::find(&buf[2..], ZIP_EOCD_MAGIC) {
|
||||
return Err(Error::EocdMagicInComment(offset));
|
||||
}
|
||||
|
||||
// Archive comment size (for the EOCD comment size field).
|
||||
@@ -735,7 +846,7 @@ pub struct StreamingSigningWriter<W> {
|
||||
impl<W: Write> StreamingSigningWriter<W> {
|
||||
pub fn new(inner: W) -> Self {
|
||||
Self {
|
||||
inner: HashingWriter::new(inner, Context::new(&ring::digest::SHA256)),
|
||||
inner: HashingWriter::new(inner, Context::new(&aws_lc_rs::digest::SHA256)),
|
||||
queue: Default::default(),
|
||||
used: 0,
|
||||
}
|
||||
@@ -743,21 +854,23 @@ impl<W: Write> StreamingSigningWriter<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(),
|
||||
);
|
||||
return Err(Error::ZipTooSmall);
|
||||
}
|
||||
|
||||
validate_eocd(&self.queue)?;
|
||||
|
||||
// Chop off the archive comment size field and write the remaining data.
|
||||
self.inner.write_all(&self.queue[..20])?;
|
||||
self.inner
|
||||
.write_all(&self.queue[..20])
|
||||
.map_err(|e| Error::DataWrite("eocd_minus_comment", e))?;
|
||||
|
||||
let (mut raw_writer, context) = self.inner.finish();
|
||||
let digest = context.finish();
|
||||
|
||||
let size_and_comment = compute_signature_comment(key, cert, digest)?;
|
||||
raw_writer.write_all(&size_and_comment)?;
|
||||
raw_writer
|
||||
.write_all(&size_and_comment)
|
||||
.map_err(|e| Error::DataWrite("size_and_comment", e))?;
|
||||
|
||||
Ok(raw_writer)
|
||||
}
|
||||
@@ -817,34 +930,39 @@ impl<W: Read + Write + Seek> SeekableSigningWriter<W> {
|
||||
cert: &Certificate,
|
||||
cancel_signal: &AtomicBool,
|
||||
) -> Result<W> {
|
||||
let file_size = self.seek(SeekFrom::End(0))?;
|
||||
let file_size = self
|
||||
.seek(SeekFrom::End(0))
|
||||
.map_err(|e| Error::DataRead("file_size", e))?;
|
||||
|
||||
// Android only supports non-zip64 EOCD.
|
||||
if file_size < 22 {
|
||||
return Err(
|
||||
io::Error::new(io::ErrorKind::InvalidData, "Too small to contain EOCD").into(),
|
||||
);
|
||||
return Err(Error::ZipTooSmall);
|
||||
}
|
||||
|
||||
self.seek_relative(-22)?;
|
||||
let mut eocd = [0u8; 22];
|
||||
self.read_exact(&mut eocd)?;
|
||||
self.seek_relative(-22)
|
||||
.map_err(|e| Error::DataRead("eocd", e))?;
|
||||
let eocd = self
|
||||
.read_array_exact::<22>()
|
||||
.map_err(|e| Error::DataRead("eocd", e))?;
|
||||
|
||||
validate_eocd(&eocd)?;
|
||||
|
||||
// Compute the digest of everything up until the comment size field.
|
||||
let mut hashing_writer = HashingWriter::new(
|
||||
io::sink(),
|
||||
ring::digest::Context::new(&ring::digest::SHA256),
|
||||
aws_lc_rs::digest::Context::new(&aws_lc_rs::digest::SHA256),
|
||||
);
|
||||
|
||||
self.rewind()?;
|
||||
stream::copy_n(&mut self, &mut hashing_writer, file_size - 2, cancel_signal)?;
|
||||
self.rewind().map_err(|e| Error::DataRead("raw_data", e))?;
|
||||
stream::copy_n(&mut self, &mut hashing_writer, file_size - 2, cancel_signal)
|
||||
.map_err(|e| Error::DataRead("raw_data", e))?;
|
||||
|
||||
let digest = hashing_writer.finish().1.finish();
|
||||
|
||||
let size_and_comment = compute_signature_comment(key, cert, digest)?;
|
||||
self.inner.write_all(&size_and_comment)?;
|
||||
self.inner
|
||||
.write_all(&size_and_comment)
|
||||
.map_err(|e| Error::DataWrite("size_and_comment", e))?;
|
||||
|
||||
Ok(self.inner)
|
||||
}
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2023 Andrew Gunnerson
|
||||
* SPDX-License-Identifier: GPL-3.0-only
|
||||
*/
|
||||
// SPDX-FileCopyrightText: 2023-2024 Andrew Gunnerson
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use std::io::{self, Read, Seek, Write};
|
||||
|
||||
@@ -47,3 +45,35 @@ pub fn write_zeros(mut writer: impl Write + Seek, page_size: u64) -> io::Result<
|
||||
|
||||
Ok(padding)
|
||||
}
|
||||
|
||||
pub trait ZeroPadding {
|
||||
/// Trim trailing zeros. Intermediate zeros before the last non-zero byte
|
||||
/// are kept.
|
||||
fn trim_end_padding(&self) -> &[u8];
|
||||
|
||||
/// Return the slice as an array padded with zeros at the end.
|
||||
fn to_padded_array<const N: usize>(&self) -> Option<[u8; N]>;
|
||||
}
|
||||
|
||||
impl ZeroPadding for [u8] {
|
||||
fn trim_end_padding(&self) -> &[u8] {
|
||||
let first_ending_zero = self
|
||||
.iter()
|
||||
.rposition(|b| *b != 0)
|
||||
.map(|pos| pos + 1)
|
||||
.unwrap_or_default();
|
||||
|
||||
&self[..first_ending_zero]
|
||||
}
|
||||
|
||||
fn to_padded_array<const N: usize>(&self) -> Option<[u8; N]> {
|
||||
if self.len() > N {
|
||||
return None;
|
||||
}
|
||||
|
||||
let mut result = [0u8; N];
|
||||
result[..self.len()].copy_from_slice(self);
|
||||
|
||||
Some(result)
|
||||
}
|
||||
}
|
||||
|
||||
+229
-164
@@ -1,7 +1,5 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2022-2024 Andrew Gunnerson
|
||||
* SPDX-License-Identifier: GPL-3.0-only
|
||||
*/
|
||||
// SPDX-FileCopyrightText: 2022-2024 Andrew Gunnerson
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use std::{
|
||||
collections::{HashMap, HashSet},
|
||||
@@ -11,25 +9,26 @@ use std::{
|
||||
sync::atomic::AtomicBool,
|
||||
};
|
||||
|
||||
use aws_lc_rs::digest::{Context, Digest};
|
||||
use base64::engine::general_purpose::STANDARD;
|
||||
use base64::Engine;
|
||||
use byteorder::{BigEndian, ReadBytesExt};
|
||||
use bzip2::write::BzDecoder;
|
||||
use flate2::{write::GzEncoder, Compression};
|
||||
use liblzma::{
|
||||
stream::{Check, Stream},
|
||||
write::XzDecoder,
|
||||
write::XzEncoder,
|
||||
};
|
||||
use num_traits::ToPrimitive;
|
||||
use prost::Message;
|
||||
use rayon::{
|
||||
iter::{IndexedParallelIterator, IntoParallelRefMutIterator},
|
||||
prelude::{IntoParallelIterator, IntoParallelRefIterator, ParallelIterator},
|
||||
};
|
||||
use ring::digest::{Context, Digest};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use thiserror::Error;
|
||||
use x509_cert::Certificate;
|
||||
use zerocopy::{big_endian, FromBytes, IntoBytes};
|
||||
use zerocopy_derive::{FromBytes, Immutable, IntoBytes, KnownLayout, Unaligned};
|
||||
|
||||
use crate::{
|
||||
crypto::{self, RsaPublicKeyExt, RsaSigningKey, SignatureAlgorithm},
|
||||
@@ -38,14 +37,14 @@ use crate::{
|
||||
InstallOperation, PartitionInfo, PartitionUpdate, Signatures,
|
||||
},
|
||||
stream::{
|
||||
self, CountingReader, FromReader, HashingWriter, ReadDiscardExt, ReadSeekReopen, WriteSeek,
|
||||
WriteSeekReopen,
|
||||
self, CountingReader, FromReader, HashingWriter, ReadDiscardExt, ReadFixedSizeExt,
|
||||
ReadSeekReopen, WriteSeek, WriteSeekReopen,
|
||||
},
|
||||
util,
|
||||
util::{self, OutOfBoundsError},
|
||||
};
|
||||
|
||||
const OTA_MAGIC: &[u8; 4] = b"CrAU";
|
||||
const OTA_HEADER_SIZE: usize = OTA_MAGIC.len() + 8 + 8 + 4;
|
||||
const PAYLOAD_MAGIC: &[u8; 4] = b"CrAU";
|
||||
const PAYLOAD_VERSION: u64 = 2;
|
||||
|
||||
const MANIFEST_MAX_SIZE: usize = 4 * 1024 * 1024;
|
||||
|
||||
@@ -93,19 +92,64 @@ pub enum Error {
|
||||
#[error("{0:?} field is missing")]
|
||||
MissingField(&'static str),
|
||||
#[error("{0:?} field is out of bounds")]
|
||||
FieldOutOfBounds(&'static str),
|
||||
#[error("Crypto error")]
|
||||
Crypto(#[from] crypto::Error),
|
||||
#[error("Failed to decode protobuf message")]
|
||||
ProtobufDecode(#[from] prost::DecodeError),
|
||||
#[error("XZ stream error")]
|
||||
XzStream(#[from] liblzma::stream::Error),
|
||||
#[error("I/O error")]
|
||||
Io(#[from] io::Error),
|
||||
IntOutOfBounds(&'static str, #[source] OutOfBoundsError),
|
||||
#[error("{0:?} overflowed integer bounds during calculations")]
|
||||
IntOverflow(&'static str),
|
||||
#[error("Failed to decode payload manifest protobuf message")]
|
||||
ManifestDecode(#[source] prost::DecodeError),
|
||||
#[error("Failed to decode payload signatures protobuf message")]
|
||||
SignaturesDecode(#[source] prost::DecodeError),
|
||||
#[error("Failed to generate payload signature")]
|
||||
SignatureGenerate(#[source] crypto::Error),
|
||||
#[error("Failed to verify payload signature")]
|
||||
SignatureVerify(#[source] crypto::Error),
|
||||
#[error("Failed to read payload data: {0}")]
|
||||
DataRead(&'static str, #[source] io::Error),
|
||||
#[error("Failed to write payload data: {0}")]
|
||||
DataWrite(&'static str, #[source] io::Error),
|
||||
#[error("Expected {expected} bytes, but only wrote {actual} bytes")]
|
||||
UnwrittenData { actual: u64, expected: u64 },
|
||||
#[error("I/O error when applying {op_type:?} operation for {num_blocks} blocks starting at {start_block}")]
|
||||
OperationApply {
|
||||
op_type: Type,
|
||||
start_block: u64,
|
||||
num_blocks: u64,
|
||||
source: io::Error,
|
||||
},
|
||||
#[error("Failed to reopen payload")]
|
||||
PayloadReopen(#[source] io::Error),
|
||||
#[error("Failed to open input file for partition: {0}")]
|
||||
InputOpen(String, #[source] io::Error),
|
||||
#[error("Failed to open output file for partition: {0}")]
|
||||
OutputOpen(String, #[source] io::Error),
|
||||
#[error("Failed to GZ compress partition image chunk")]
|
||||
GzCompress(#[source] io::Error),
|
||||
#[error("Failed to initialize XZ encoder")]
|
||||
XzInit(#[source] liblzma::stream::Error),
|
||||
#[error("Failed to XZ compress partition image chunk")]
|
||||
XzCompress(#[source] io::Error),
|
||||
#[error("Failed to read uncompressed input partition image chunk")]
|
||||
ChunkRead(#[source] io::Error),
|
||||
#[error("Failed to write XZ-compressed output partition image chunk")]
|
||||
ChunkWrite(#[source] io::Error),
|
||||
}
|
||||
|
||||
type Result<T> = std::result::Result<T, Error>;
|
||||
|
||||
/// Raw on-disk layout for the payload header.
|
||||
#[derive(Clone, Copy, FromBytes, IntoBytes, KnownLayout, Immutable, Unaligned)]
|
||||
#[repr(packed)]
|
||||
struct RawHeader {
|
||||
/// Magic value. This should be equal to [`PAYLOAD_MAGIC`].
|
||||
magic: [u8; 4],
|
||||
/// Image version. This should be equal to [`PAYLOAD_VERSION`].
|
||||
file_format_version: big_endian::U64,
|
||||
/// Size of the [`DeltaArchiveManifest`] blob.
|
||||
manifest_size: big_endian::U64,
|
||||
/// Size of the [`Signatures`] blob.
|
||||
metadata_signature_size: big_endian::U32,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
pub struct PayloadHeader {
|
||||
pub version: u64,
|
||||
@@ -133,42 +177,39 @@ impl<R: Read> FromReader<R> for PayloadHeader {
|
||||
fn from_reader(reader: R) -> Result<Self> {
|
||||
let mut reader = CountingReader::new(reader);
|
||||
|
||||
let mut magic = [0u8; 4];
|
||||
reader.read_exact(&mut magic)?;
|
||||
if magic != *OTA_MAGIC {
|
||||
return Err(Error::UnknownMagic(magic));
|
||||
let header =
|
||||
RawHeader::read_from_io(&mut reader).map_err(|e| Error::DataRead("header", e))?;
|
||||
|
||||
if header.magic != *PAYLOAD_MAGIC {
|
||||
return Err(Error::UnknownMagic(header.magic));
|
||||
}
|
||||
|
||||
let version = reader.read_u64::<BigEndian>()?;
|
||||
if version != 2 {
|
||||
return Err(Error::UnsupportedVersion(version));
|
||||
if header.file_format_version != PAYLOAD_VERSION {
|
||||
return Err(Error::UnsupportedVersion(header.file_format_version.get()));
|
||||
}
|
||||
|
||||
let manifest_size = reader
|
||||
.read_u64::<BigEndian>()?
|
||||
.to_usize()
|
||||
.and_then(|s| {
|
||||
if s <= MANIFEST_MAX_SIZE {
|
||||
Some(s)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.ok_or_else(|| Error::FieldOutOfBounds("manifest_size"))?;
|
||||
let metadata_signature_size = reader.read_u32::<BigEndian>()?;
|
||||
let manifest_size: usize = util::try_cast(header.manifest_size.get())
|
||||
.and_then(|s| util::check_bounds(s, ..=MANIFEST_MAX_SIZE))
|
||||
.map_err(|e| Error::IntOutOfBounds("manifest_size", e))?;
|
||||
|
||||
let mut manifest_raw = vec![0u8; manifest_size];
|
||||
reader.read_exact(&mut manifest_raw)?;
|
||||
let manifest = DeltaArchiveManifest::decode(manifest_raw.as_slice())?;
|
||||
let manifest_raw = reader
|
||||
.read_vec_exact(manifest_size)
|
||||
.map_err(|e| Error::DataRead("manifest", e))?;
|
||||
let manifest =
|
||||
DeltaArchiveManifest::decode(manifest_raw.as_slice()).map_err(Error::ManifestDecode)?;
|
||||
|
||||
// Skip manifest signatures.
|
||||
reader.read_discard_exact(metadata_signature_size.into())?;
|
||||
reader
|
||||
.read_discard_exact(header.metadata_signature_size.into())
|
||||
.map_err(|e| Error::DataRead("metadata_signatures", e))?;
|
||||
|
||||
Ok(Self {
|
||||
version,
|
||||
version: header.file_format_version.get(),
|
||||
manifest,
|
||||
metadata_signature_size,
|
||||
blob_offset: reader.stream_position()?,
|
||||
metadata_signature_size: header.metadata_signature_size.get(),
|
||||
blob_offset: reader
|
||||
.stream_position()
|
||||
.map_err(|e| Error::DataRead("blob_offset", e))?,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -176,7 +217,9 @@ 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: &RsaSigningKey) -> Result<Signatures> {
|
||||
let mut digest_signed = key.sign(SignatureAlgorithm::Sha256WithRsa, digest)?;
|
||||
let mut digest_signed = key
|
||||
.sign(SignatureAlgorithm::Sha256WithRsa, digest)
|
||||
.map_err(Error::SignatureGenerate)?;
|
||||
assert!(
|
||||
digest_signed.len() <= key.size(),
|
||||
"Signature exceeds maximum size",
|
||||
@@ -201,25 +244,25 @@ fn sign_digest(digest: &[u8], key: &RsaSigningKey) -> Result<Signatures> {
|
||||
|
||||
/// Verify `digest` inside `signatures` using `cert`.
|
||||
fn verify_digest(digest: &[u8], signatures: &Signatures, cert: &Certificate) -> Result<()> {
|
||||
let public_key = crypto::get_public_key(cert)?;
|
||||
let public_key = crypto::get_public_key(cert).map_err(Error::SignatureVerify)?;
|
||||
let mut last_error = None;
|
||||
|
||||
for signature in &signatures.signatures {
|
||||
let Some(data) = &signature.data else {
|
||||
continue;
|
||||
};
|
||||
let Some(size) = signature.unpadded_signature_size else {
|
||||
continue;
|
||||
};
|
||||
let without_padding = &data[..size as usize];
|
||||
let size = signature
|
||||
.unpadded_signature_size
|
||||
.map_or(data.len(), |s| s as usize);
|
||||
let without_padding = &data[..size];
|
||||
|
||||
match public_key.verify_sig(SignatureAlgorithm::Sha256WithRsa, digest, without_padding) {
|
||||
Ok(_) => return Ok(()),
|
||||
Err(e) => last_error = Some(e),
|
||||
Ok(()) => return Ok(()),
|
||||
Err(e) => last_error = Some(Error::SignatureVerify(e)),
|
||||
}
|
||||
}
|
||||
|
||||
Err(last_error.map_or(Error::NoSignatures, |e| e.into()))
|
||||
Err(last_error.unwrap_or(Error::NoSignatures))
|
||||
}
|
||||
|
||||
fn parse_properties(data: &str) -> Result<HashMap<String, String>> {
|
||||
@@ -331,7 +374,7 @@ impl<W: Write> PayloadWriter<W> {
|
||||
// Get the length of an dummy signature struct since the length fields
|
||||
// are part of the data to be signed.
|
||||
let dummy_sig = sign_digest(
|
||||
ring::digest::digest(&ring::digest::SHA256, b"").as_ref(),
|
||||
aws_lc_rs::digest::digest(&aws_lc_rs::digest::SHA256, b"").as_ref(),
|
||||
&key,
|
||||
)?;
|
||||
let dummy_sig_size = dummy_sig.encoded_len();
|
||||
@@ -344,39 +387,37 @@ impl<W: Write> PayloadWriter<W> {
|
||||
let manifest_raw_new = header.manifest.encode_to_vec();
|
||||
|
||||
// Excludes signatures (hashes are for signing).
|
||||
let mut h_partial = Context::new(&ring::digest::SHA256);
|
||||
let mut h_partial = Context::new(&aws_lc_rs::digest::SHA256);
|
||||
// Includes signatures (hashes are for properties file).
|
||||
let mut h_full = Context::new(&ring::digest::SHA256);
|
||||
let mut h_full = Context::new(&aws_lc_rs::digest::SHA256);
|
||||
|
||||
// Write header to output file.
|
||||
write_hash!(inner, [h_partial, h_full], OTA_MAGIC)?;
|
||||
write_hash!(inner, [h_partial, h_full], &header.version.to_be_bytes())?;
|
||||
write_hash!(
|
||||
inner,
|
||||
[h_partial, h_full],
|
||||
&(manifest_raw_new.len() as u64).to_be_bytes(),
|
||||
)?;
|
||||
write_hash!(
|
||||
inner,
|
||||
[h_partial, h_full],
|
||||
&(dummy_sig_size as u32).to_be_bytes()
|
||||
)?;
|
||||
let raw_header = RawHeader {
|
||||
magic: *PAYLOAD_MAGIC,
|
||||
file_format_version: header.version.into(),
|
||||
manifest_size: (manifest_raw_new.len() as u64).into(),
|
||||
metadata_signature_size: (dummy_sig_size as u32).into(),
|
||||
};
|
||||
write_hash!(inner, [h_partial, h_full], raw_header.as_bytes())
|
||||
.map_err(|e| Error::DataWrite("header", e))?;
|
||||
|
||||
// Write new manifest.
|
||||
write_hash!(inner, [h_partial, h_full], &manifest_raw_new)?;
|
||||
write_hash!(inner, [h_partial, h_full], &manifest_raw_new)
|
||||
.map_err(|e| Error::DataWrite("manifest", e))?;
|
||||
|
||||
// Sign metadata (header + manifest) hash. The signature is not included
|
||||
// in the payload hash.
|
||||
let metadata_hash = h_partial.clone().finish();
|
||||
let metadata_sig = sign_digest(metadata_hash.as_ref(), &key)?;
|
||||
let metadata_sig_raw = metadata_sig.encode_to_vec();
|
||||
write_hash!(inner, [h_full], &metadata_sig_raw)?;
|
||||
write_hash!(inner, [h_full], &metadata_sig_raw)
|
||||
.map_err(|e| Error::DataWrite("metadata_signatures", e))?;
|
||||
|
||||
Ok(Self {
|
||||
inner,
|
||||
header,
|
||||
metadata_hash,
|
||||
metadata_size: OTA_HEADER_SIZE + manifest_raw_new.len(),
|
||||
metadata_size: raw_header.as_bytes().len() + manifest_raw_new.len(),
|
||||
partition_index: None,
|
||||
operation_index: None,
|
||||
done: false,
|
||||
@@ -398,7 +439,8 @@ impl<W: Write> PayloadWriter<W> {
|
||||
let payload_partial_hash = self.h_partial.clone().finish();
|
||||
let payload_sig = sign_digest(payload_partial_hash.as_ref(), &self.key)?;
|
||||
let payload_sig_raw = payload_sig.encode_to_vec();
|
||||
write_hash!(self.inner, [self.h_full], &payload_sig_raw)?;
|
||||
write_hash!(self.inner, [self.h_full], &payload_sig_raw)
|
||||
.map_err(|e| Error::DataWrite("payload_signatures", e))?;
|
||||
|
||||
// Everything before the blob.
|
||||
let metadata_with_sig_size =
|
||||
@@ -423,23 +465,20 @@ impl<W: Write> PayloadWriter<W> {
|
||||
/// Prepare for writing the next source data blob corresponding to an
|
||||
/// [`InstallOperation`]. To write all of the payload data, call this method
|
||||
/// followed by [`Self::write()`] repeatedly until `Ok(false)` is returned
|
||||
/// or an error occurs. This function will fail if the amount of data
|
||||
/// written for the previous operation does not match
|
||||
/// or an error occurs. [`Error::UnwrittenData`] will be returned if the
|
||||
/// amount of data written for the previous operation does not match
|
||||
/// [`InstallOperation::data_length`].
|
||||
pub fn begin_next_operation(&mut self) -> Result<bool> {
|
||||
if let Some(operation) = self.operation() {
|
||||
// Only operations that reference data in the blob will have a
|
||||
// length set.
|
||||
if self.written < operation.data_length.unwrap_or(0) {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidInput,
|
||||
format!(
|
||||
"Expected {} bytes, but only wrote {} bytes",
|
||||
operation.data_length.unwrap(),
|
||||
self.written,
|
||||
),
|
||||
)
|
||||
.into());
|
||||
let expected = operation.data_length.unwrap_or(0);
|
||||
|
||||
if self.written < expected {
|
||||
return Err(Error::UnwrittenData {
|
||||
actual: self.written,
|
||||
expected,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -549,21 +588,21 @@ pub fn verify_payload(
|
||||
cancel_signal: &AtomicBool,
|
||||
) -> Result<()> {
|
||||
let header = PayloadHeader::from_reader(&mut reader)?;
|
||||
reader.rewind()?;
|
||||
reader.rewind().map_err(|e| Error::DataRead("header", e))?;
|
||||
|
||||
let payload_signatures_offset = header
|
||||
.manifest
|
||||
.signatures_offset
|
||||
.ok_or_else(|| Error::MissingField("signatures_offset"))?;
|
||||
.ok_or(Error::MissingField("signatures_offset"))?;
|
||||
let payload_signatures_size = header
|
||||
.manifest
|
||||
.signatures_size
|
||||
.ok_or_else(|| Error::MissingField("signatures_size"))?;
|
||||
.ok_or(Error::MissingField("signatures_size"))?;
|
||||
|
||||
// Excludes signatures (hashes are for signing).
|
||||
let mut h_partial = Context::new(&ring::digest::SHA256);
|
||||
let mut h_partial = Context::new(&aws_lc_rs::digest::SHA256);
|
||||
// Includes signatures (hashes are for properties file).
|
||||
let mut h_full = Context::new(&ring::digest::SHA256);
|
||||
let mut h_full = Context::new(&aws_lc_rs::digest::SHA256);
|
||||
|
||||
// Read from the beginning to the metadata signature.
|
||||
let metadata_size = header.blob_offset - u64::from(header.metadata_signature_size);
|
||||
@@ -576,7 +615,8 @@ pub fn verify_payload(
|
||||
h_full.update(data);
|
||||
},
|
||||
cancel_signal,
|
||||
)?;
|
||||
)
|
||||
.map_err(|e| Error::DataRead("metadata", e))?;
|
||||
let metadata_hash = h_partial.clone().finish();
|
||||
|
||||
// Read the metadata signatures.
|
||||
@@ -589,10 +629,11 @@ pub fn verify_payload(
|
||||
header.metadata_signature_size.into(),
|
||||
|data| h_full.update(data),
|
||||
cancel_signal,
|
||||
)?;
|
||||
)
|
||||
.map_err(|e| Error::DataRead("metadata_signatures", e))?;
|
||||
|
||||
let buf = writer.into_inner();
|
||||
Signatures::decode(buf.as_slice())?
|
||||
Signatures::decode(buf.as_slice()).map_err(Error::SignaturesDecode)?
|
||||
};
|
||||
|
||||
// Check the metadata signatures.
|
||||
@@ -600,7 +641,9 @@ pub fn verify_payload(
|
||||
|
||||
// Check the blob offset.
|
||||
{
|
||||
let actual = reader.stream_position()?;
|
||||
let actual = reader
|
||||
.stream_position()
|
||||
.map_err(|e| Error::DataRead("blob_offset", e))?;
|
||||
if header.blob_offset != actual {
|
||||
return Err(Error::InvalidBlobOffset {
|
||||
expected: header.blob_offset,
|
||||
@@ -619,13 +662,16 @@ pub fn verify_payload(
|
||||
h_full.update(data);
|
||||
},
|
||||
cancel_signal,
|
||||
)?;
|
||||
)
|
||||
.map_err(|e| Error::DataRead("blob", e))?;
|
||||
let payload_hash = h_partial.clone().finish();
|
||||
|
||||
// Check the payload signatures offset.
|
||||
{
|
||||
let expected = header.blob_offset + payload_signatures_offset;
|
||||
let actual = reader.stream_position()?;
|
||||
let actual = reader
|
||||
.stream_position()
|
||||
.map_err(|e| Error::DataRead("payload_signatures_offset", e))?;
|
||||
if expected != actual {
|
||||
return Err(Error::InvalidPayloadSignaturesOffset { expected, actual });
|
||||
}
|
||||
@@ -641,10 +687,11 @@ pub fn verify_payload(
|
||||
payload_signatures_size,
|
||||
|data| h_full.update(data),
|
||||
cancel_signal,
|
||||
)?;
|
||||
)
|
||||
.map_err(|e| Error::DataRead("payload_signatures", e))?;
|
||||
|
||||
let buf = writer.into_inner();
|
||||
Signatures::decode(buf.as_slice())?
|
||||
Signatures::decode(buf.as_slice()).map_err(Error::SignaturesDecode)?
|
||||
};
|
||||
|
||||
// Check the payload signatures.
|
||||
@@ -653,7 +700,9 @@ pub fn verify_payload(
|
||||
// Check properties file.
|
||||
let expected_properties_raw = generate_properties(
|
||||
h_full.finish().as_ref(),
|
||||
reader.stream_position()?,
|
||||
reader
|
||||
.stream_position()
|
||||
.map_err(|e| Error::DataRead("payload_size", e))?,
|
||||
metadata_hash.as_ref(),
|
||||
metadata_size,
|
||||
);
|
||||
@@ -688,21 +737,26 @@ pub fn apply_operation(
|
||||
for extent in &op.dst_extents {
|
||||
let start_block = extent
|
||||
.start_block
|
||||
.ok_or_else(|| Error::MissingField("start_block"))?;
|
||||
let num_blocks = extent
|
||||
.num_blocks
|
||||
.ok_or_else(|| Error::MissingField("num_blocks"))?;
|
||||
.ok_or(Error::MissingField("start_block"))?;
|
||||
let num_blocks = extent.num_blocks.ok_or(Error::MissingField("num_blocks"))?;
|
||||
|
||||
let out_offset = start_block
|
||||
.checked_mul(block_size.into())
|
||||
.ok_or_else(|| Error::FieldOutOfBounds("out_offset"))?;
|
||||
.ok_or(Error::IntOverflow("out_offset"))?;
|
||||
let out_data_length = num_blocks
|
||||
.checked_mul(block_size.into())
|
||||
.ok_or_else(|| Error::FieldOutOfBounds("out_data_length"))?;
|
||||
.ok_or(Error::IntOverflow("out_data_length"))?;
|
||||
|
||||
writer.seek(SeekFrom::Start(out_offset))?;
|
||||
let error_fn = |e: io::Error| Error::OperationApply {
|
||||
op_type: op.r#type(),
|
||||
start_block,
|
||||
num_blocks,
|
||||
source: e,
|
||||
};
|
||||
|
||||
let mut hasher = Context::new(&ring::digest::SHA256);
|
||||
writer.seek(SeekFrom::Start(out_offset)).map_err(error_fn)?;
|
||||
|
||||
let mut hasher = Context::new(&aws_lc_rs::digest::SHA256);
|
||||
|
||||
match op.r#type() {
|
||||
// Handle ZERO/DISCARD specially since they don't require access to
|
||||
@@ -714,20 +768,17 @@ pub fn apply_operation(
|
||||
out_data_length,
|
||||
|data| hasher.update(data),
|
||||
cancel_signal,
|
||||
)?;
|
||||
)
|
||||
.map_err(error_fn)?;
|
||||
}
|
||||
other => {
|
||||
let data_offset = op
|
||||
.data_offset
|
||||
.ok_or_else(|| Error::MissingField("data_offset"))?;
|
||||
let data_length = op
|
||||
.data_length
|
||||
.ok_or_else(|| Error::MissingField("data_length"))?;
|
||||
let data_offset = op.data_offset.ok_or(Error::MissingField("data_offset"))?;
|
||||
let data_length = op.data_length.ok_or(Error::MissingField("data_length"))?;
|
||||
let in_offset = blob_offset
|
||||
.checked_add(data_offset)
|
||||
.ok_or_else(|| Error::FieldOutOfBounds("in_offset"))?;
|
||||
.ok_or(Error::IntOverflow("in_offset"))?;
|
||||
|
||||
reader.seek(SeekFrom::Start(in_offset))?;
|
||||
reader.seek(SeekFrom::Start(in_offset)).map_err(error_fn)?;
|
||||
|
||||
match other {
|
||||
Type::Replace => {
|
||||
@@ -737,7 +788,8 @@ pub fn apply_operation(
|
||||
data_length,
|
||||
|data| hasher.update(data),
|
||||
cancel_signal,
|
||||
)?;
|
||||
)
|
||||
.map_err(error_fn)?;
|
||||
}
|
||||
Type::ReplaceBz => {
|
||||
let mut decoder = BzDecoder::new(&mut writer);
|
||||
@@ -747,8 +799,9 @@ pub fn apply_operation(
|
||||
data_length,
|
||||
|data| hasher.update(data),
|
||||
cancel_signal,
|
||||
)?;
|
||||
decoder.finish()?;
|
||||
)
|
||||
.and_then(|()| decoder.finish())
|
||||
.map_err(error_fn)?;
|
||||
}
|
||||
Type::ReplaceXz => {
|
||||
let mut decoder = XzDecoder::new(&mut writer);
|
||||
@@ -758,8 +811,9 @@ pub fn apply_operation(
|
||||
data_length,
|
||||
|data| hasher.update(data),
|
||||
cancel_signal,
|
||||
)?;
|
||||
decoder.finish()?;
|
||||
)
|
||||
.and_then(|()| decoder.finish())
|
||||
.map_err(error_fn)?;
|
||||
}
|
||||
_ => return Err(Error::UnsupportedOperation(op.r#type())),
|
||||
}
|
||||
@@ -801,8 +855,10 @@ pub fn extract_image(
|
||||
.operations
|
||||
.par_iter()
|
||||
.map(|op| -> Result<()> {
|
||||
let reader = payload.reopen_boxed()?;
|
||||
let writer = output.reopen_boxed()?;
|
||||
let reader = payload.reopen_boxed().map_err(Error::PayloadReopen)?;
|
||||
let writer = output
|
||||
.reopen_boxed()
|
||||
.map_err(|e| Error::OutputOpen(partition_name.to_owned(), e))?;
|
||||
|
||||
apply_operation(
|
||||
reader,
|
||||
@@ -849,8 +905,8 @@ pub fn extract_images<'a>(
|
||||
operations
|
||||
.into_par_iter()
|
||||
.map(|(name, op)| -> Result<()> {
|
||||
let reader = payload.reopen_boxed()?;
|
||||
let writer = open_output(name)?;
|
||||
let reader = payload.reopen_boxed().map_err(Error::PayloadReopen)?;
|
||||
let writer = open_output(name).map_err(|e| Error::OutputOpen(name.to_owned(), e))?;
|
||||
|
||||
apply_operation(
|
||||
reader,
|
||||
@@ -869,18 +925,19 @@ pub fn extract_images<'a>(
|
||||
fn compress_chunk(raw_data: &[u8], cancel_signal: &AtomicBool) -> Result<(Vec<u8>, Digest)> {
|
||||
let reader = Cursor::new(raw_data);
|
||||
let writer = Cursor::new(Vec::new());
|
||||
let hashing_writer = HashingWriter::new(writer, Context::new(&ring::digest::SHA256));
|
||||
let hashing_writer = HashingWriter::new(writer, Context::new(&aws_lc_rs::digest::SHA256));
|
||||
|
||||
// AOSP's payload_consumer does not support checking CRC during
|
||||
// decompression. Also, we intentionally pick the lowest compression level
|
||||
// since we primarily care about squishing zeros. The non-zero portions of
|
||||
// boot images are usually already-compressed kernels and ramdisks.
|
||||
let stream = Stream::new_easy_encoder(0, Check::None)?;
|
||||
let stream = Stream::new_easy_encoder(0, Check::None).map_err(Error::XzInit)?;
|
||||
let mut xz_writer = XzEncoder::new_stream(hashing_writer, stream);
|
||||
|
||||
stream::copy_n(reader, &mut xz_writer, raw_data.len() as u64, cancel_signal)?;
|
||||
stream::copy_n(reader, &mut xz_writer, raw_data.len() as u64, cancel_signal)
|
||||
.map_err(Error::XzCompress)?;
|
||||
|
||||
let hashing_writer = xz_writer.finish()?;
|
||||
let hashing_writer = xz_writer.finish().map_err(Error::XzCompress)?;
|
||||
let (writer, context_compressed) = hashing_writer.finish();
|
||||
let digest_compressed = context_compressed.finish();
|
||||
let data = writer.into_inner();
|
||||
@@ -903,17 +960,20 @@ impl VabcAlgo {
|
||||
}
|
||||
}
|
||||
|
||||
fn compressed_size(&self, mut raw_data: &[u8], block_size: u32) -> u64 {
|
||||
fn compressed_size(self, mut raw_data: &[u8], block_size: u32) -> Result<u64> {
|
||||
let mut total = 0;
|
||||
|
||||
while !raw_data.is_empty() {
|
||||
let n = raw_data.len().min(block_size as usize);
|
||||
// This should match CompressWorker::GetDefaultCompressionLevel() in
|
||||
// AOSP's libsnapshot.
|
||||
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),
|
||||
Self::Gzip => {
|
||||
let mut encoder = GzEncoder::new(Vec::new(), Compression::best());
|
||||
encoder.write_all(raw_data).map_err(Error::GzCompress)?;
|
||||
encoder.finish().map_err(Error::GzCompress)?
|
||||
}
|
||||
};
|
||||
|
||||
total += compressed.len().min(n) as u64;
|
||||
@@ -921,7 +981,7 @@ impl VabcAlgo {
|
||||
raw_data = &raw_data[n..];
|
||||
}
|
||||
|
||||
total
|
||||
Ok(total)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -959,7 +1019,10 @@ pub fn compress_image(
|
||||
const CHUNK_SIZE: u64 = 2 * 1024 * 1024;
|
||||
const CHUNK_GROUP: u64 = 32;
|
||||
|
||||
let file_size = input.reopen_boxed()?.seek(SeekFrom::End(0))?;
|
||||
let file_size = input
|
||||
.reopen_boxed()
|
||||
.and_then(|mut r| r.seek(SeekFrom::End(0)))
|
||||
.map_err(|e| Error::InputOpen(partition_name.to_owned(), e))?;
|
||||
let final_chunk_different = file_size % CHUNK_SIZE != 0;
|
||||
|
||||
if file_size % u64::from(block_size) != 0 || CHUNK_SIZE % u64::from(block_size) != 0 {
|
||||
@@ -972,7 +1035,7 @@ pub fn compress_image(
|
||||
|
||||
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 context_uncompressed = Context::new(&aws_lc_rs::digest::SHA256);
|
||||
let mut cow_estimate = 0;
|
||||
let mut operations = vec![];
|
||||
|
||||
@@ -985,7 +1048,7 @@ pub fn compress_image(
|
||||
|
||||
let uncompressed_data_group = (chunks_done..chunks_done + chunks_group)
|
||||
.into_par_iter()
|
||||
.map(|chunk| -> Result<(u64, Vec<u8>)> {
|
||||
.map(|chunk| -> io::Result<(u64, Vec<u8>)> {
|
||||
let mut reader = input.reopen_boxed()?;
|
||||
let offset = reader.seek(SeekFrom::Start(chunk * CHUNK_SIZE))?;
|
||||
|
||||
@@ -994,14 +1057,14 @@ pub fn compress_image(
|
||||
} else {
|
||||
CHUNK_SIZE
|
||||
};
|
||||
let mut data = vec![0u8; chunk_size as usize];
|
||||
|
||||
stream::check_cancel(cancel_signal)?;
|
||||
reader.read_exact(&mut data)?;
|
||||
let data = reader.read_vec_exact(chunk_size as usize)?;
|
||||
|
||||
Ok((offset, data))
|
||||
})
|
||||
.collect::<Result<Vec<_>>>()?;
|
||||
.collect::<io::Result<Vec<_>>>()
|
||||
.map_err(Error::ChunkRead)?;
|
||||
|
||||
for (_, data) in &uncompressed_data_group {
|
||||
context_uncompressed.update(data);
|
||||
@@ -1012,11 +1075,10 @@ pub fn compress_image(
|
||||
.map(
|
||||
|(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 cow_size = vabc_algo
|
||||
.map(|a| a.compressed_size(&raw_data, block_size))
|
||||
.transpose()?
|
||||
.unwrap_or(0);
|
||||
|
||||
let extent = Extent {
|
||||
start_block: Some(raw_offset / u64::from(block_size)),
|
||||
@@ -1042,14 +1104,15 @@ pub fn compress_image(
|
||||
|
||||
let group_operations = compressed_data_group
|
||||
.into_par_iter()
|
||||
.map(|(data, operation, _)| -> Result<InstallOperation> {
|
||||
.map(|(data, operation, _)| -> io::Result<InstallOperation> {
|
||||
let mut writer = output.reopen_boxed()?;
|
||||
writer.seek(SeekFrom::Start(operation.data_offset.unwrap()))?;
|
||||
writer.write_all(&data)?;
|
||||
|
||||
Ok(operation)
|
||||
})
|
||||
.collect::<Result<Vec<_>>>()?;
|
||||
.collect::<io::Result<Vec<_>>>()
|
||||
.map_err(Error::ChunkWrite)?;
|
||||
|
||||
operations.extend(group_operations.into_iter());
|
||||
}
|
||||
@@ -1143,7 +1206,7 @@ pub fn compress_modified_image(
|
||||
|
||||
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 context_uncompressed = Context::new(&aws_lc_rs::digest::SHA256);
|
||||
let mut modified_operations = vec![];
|
||||
|
||||
// Read the file one group at a time. This allows for some parallelization
|
||||
@@ -1160,28 +1223,29 @@ pub fn compress_modified_image(
|
||||
let extents_start = operation.dst_extents[0]
|
||||
.start_block()
|
||||
.checked_mul(u64::from(block_size))
|
||||
.ok_or_else(|| Error::FieldOutOfBounds("extents_start"))?;
|
||||
.ok_or(Error::IntOverflow("extents_start"))?;
|
||||
let extents_size = operation
|
||||
.dst_extents
|
||||
.iter()
|
||||
.map(|e| e.num_blocks())
|
||||
.try_fold(0u64, |acc, n| acc.checked_add(n))
|
||||
.and_then(|n| n.checked_mul(u64::from(block_size)))
|
||||
.ok_or_else(|| Error::FieldOutOfBounds("extents_size"))?;
|
||||
.ok_or(Error::IntOverflow("extents_size"))?;
|
||||
let extents_end = extents_start
|
||||
.checked_add(extents_size)
|
||||
.ok_or_else(|| Error::FieldOutOfBounds("extents_end"))?;
|
||||
let extents_size = extents_size
|
||||
.to_usize()
|
||||
.ok_or_else(|| Error::FieldOutOfBounds("extents_size"))?;
|
||||
.ok_or(Error::IntOverflow("extents_end"))?;
|
||||
let extents_size: usize = util::try_cast(extents_size)
|
||||
.map_err(|e| Error::IntOutOfBounds("extents_size", e))?;
|
||||
|
||||
let mut reader = input.reopen_boxed()?;
|
||||
reader.seek(SeekFrom::Start(extents_start))?;
|
||||
let mut reader = input.reopen_boxed().map_err(Error::ChunkRead)?;
|
||||
reader
|
||||
.seek(SeekFrom::Start(extents_start))
|
||||
.map_err(Error::ChunkRead)?;
|
||||
|
||||
let mut data = vec![0u8; extents_size];
|
||||
|
||||
stream::check_cancel(cancel_signal)?;
|
||||
reader.read_exact(&mut data)?;
|
||||
stream::check_cancel(cancel_signal).map_err(Error::ChunkRead)?;
|
||||
let data = reader
|
||||
.read_vec_exact(extents_size)
|
||||
.map_err(Error::ChunkRead)?;
|
||||
|
||||
let was_modified = util::ranges_overlaps(ranges, &(extents_start..extents_end));
|
||||
|
||||
@@ -1226,7 +1290,8 @@ pub fn compress_modified_image(
|
||||
|
||||
Ok(i..i + 1)
|
||||
})
|
||||
.collect::<Result<Vec<_>>>()?;
|
||||
.collect::<io::Result<Vec<_>>>()
|
||||
.map_err(Error::ChunkWrite)?;
|
||||
|
||||
modified_operations.extend(modified_group_operations);
|
||||
}
|
||||
|
||||
+161
-122
@@ -1,7 +1,5 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2024 Andrew Gunnerson
|
||||
* SPDX-License-Identifier: GPL-3.0-only
|
||||
*/
|
||||
// SPDX-FileCopyrightText: 2024 Andrew Gunnerson
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use std::{
|
||||
fmt,
|
||||
@@ -13,7 +11,10 @@ use std::{
|
||||
use crc32fast::Hasher;
|
||||
use dlv_list::{Index, VecList};
|
||||
use thiserror::Error;
|
||||
use zerocopy::{byteorder::little_endian, AsBytes, FromBytes, FromZeroes, Unaligned};
|
||||
use zerocopy::{byteorder::little_endian, FromBytes, IntoBytes};
|
||||
use zerocopy_derive::{FromBytes, Immutable, IntoBytes, KnownLayout, Unaligned};
|
||||
|
||||
use crate::stream::ReadDiscardExt;
|
||||
|
||||
/// Magic value for [`RawHeader::magic`].
|
||||
const HEADER_MAGIC: u32 = 0xed26ff3a;
|
||||
@@ -34,22 +35,77 @@ pub const MINOR_VERSION: u16 = 0;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum Error {
|
||||
#[error("Sparse header: {0}")]
|
||||
Header(String),
|
||||
#[error("Sparse chunk #{0}: {1}")]
|
||||
Chunk(u32, String),
|
||||
#[error("Sparse reader: {0}")]
|
||||
Reader(String),
|
||||
#[error("Sparse writer: {0}")]
|
||||
Writer(String),
|
||||
#[error("I/O error")]
|
||||
Io(#[from] io::Error),
|
||||
// Header errors.
|
||||
#[error("Invalid magic: {0:#010x}")]
|
||||
InvalidMagic(u32),
|
||||
#[error("Unsupported major version: {0}")]
|
||||
UnsupportedMajorVersion(u16),
|
||||
#[error("Invalid file header size: {0} < {size}", size = mem::size_of::<RawHeader>())]
|
||||
InvalidFileHeaderSize(u16),
|
||||
#[error("Invalid chunk header size: {0} < {size}", size = mem::size_of::<RawChunk>())]
|
||||
InvalidChunkHeaderSize(u16),
|
||||
#[error("Invalid block size (must be a non-zero multiple of 4): {0}")]
|
||||
InvalidBlockSize(u32),
|
||||
// Chunk errors.
|
||||
#[error("Chunk #{index}: Size overflow: {chunk_size} * {block_size}")]
|
||||
ChunkSizeOverflow {
|
||||
index: u32,
|
||||
chunk_size: u32,
|
||||
block_size: u32,
|
||||
},
|
||||
#[error("Chunk #{index}: Invalid type: {chunk_type}")]
|
||||
InvalidChunkType { index: u32, chunk_type: u16 },
|
||||
#[error("Chunk #{index}: Data size too large: {data_size}")]
|
||||
DataSizeTooLarge { index: u32, data_size: u32 },
|
||||
#[error("Chunk #{index}: Block count overflow: {start_block} + {chunk_size}")]
|
||||
BlockCountOverflow {
|
||||
index: u32,
|
||||
start_block: u32,
|
||||
chunk_size: u32,
|
||||
},
|
||||
#[error("Chunk #{index}: End block {end_block} exceeds total blocks {total_blocks}")]
|
||||
EndBlockExceedsTotal {
|
||||
index: u32,
|
||||
end_block: u32,
|
||||
total_blocks: u32,
|
||||
},
|
||||
#[error("Chunk #{index}: CRC32 chunk is not empty")]
|
||||
Crc32ChunkNotEmpty { index: u32, chunk_size: u32 },
|
||||
#[error("Chunk #{index}: Expected total size {expected_size}, but have {total_size}")]
|
||||
InvalidChunkSize {
|
||||
index: u32,
|
||||
expected_size: u32,
|
||||
total_size: u32,
|
||||
},
|
||||
// Reader errors.
|
||||
#[error("Must fully consume data when CRC validation is enabled")]
|
||||
Crc32RandomRead,
|
||||
#[error("Previous chunk still has {0} unread bytes")]
|
||||
UnreadChunkData(u32),
|
||||
#[error("Expected checkpoint CRC32 {expected:08x}, but have {actual:08x}")]
|
||||
MismatchedCrc32Checkpoint { expected: u32, actual: u32 },
|
||||
#[error("Expected final CRC32 {expected:08x}, but have {actual:08x}")]
|
||||
MismatchedCrc32Final { expected: u32, actual: u32 },
|
||||
// Writer errors.
|
||||
#[error("Minor version not supported for writing: {0}")]
|
||||
UnsupportedMinorVersion(u16),
|
||||
#[error("Previous chunk still has {0} unwritten bytes")]
|
||||
UnwrittenChunkData(u32),
|
||||
#[error("Already wrote all chunk headers")]
|
||||
TooManyChunks,
|
||||
#[error("Gap between end of last chunk {prev_end} and start of new chunk {cur_start}")]
|
||||
GapBetweenChunks { prev_end: u32, cur_start: u32 },
|
||||
// Wrapped errors.
|
||||
#[error("Failed to read sparse data: {0}")]
|
||||
DataRead(&'static str, #[source] io::Error),
|
||||
#[error("Failed to write sparse data: {0}")]
|
||||
DataWrite(&'static str, #[source] io::Error),
|
||||
}
|
||||
|
||||
type Result<T> = std::result::Result<T, Error>;
|
||||
|
||||
/// Raw on-disk layout for the header.
|
||||
#[derive(Clone, Copy, FromZeroes, FromBytes, AsBytes, Unaligned)]
|
||||
#[derive(Clone, Copy, FromBytes, IntoBytes, KnownLayout, Immutable, Unaligned)]
|
||||
#[repr(packed)]
|
||||
struct RawHeader {
|
||||
/// Magic value. This should be equal to [`HEADER_MAGIC`].
|
||||
@@ -96,44 +152,37 @@ impl fmt::Debug for RawHeader {
|
||||
impl RawHeader {
|
||||
fn validate(&self) -> Result<()> {
|
||||
if self.magic.get() != HEADER_MAGIC {
|
||||
return Err(Error::Header(format!(
|
||||
"Invalid magic: {:#010x}",
|
||||
self.magic.get(),
|
||||
)));
|
||||
return Err(Error::InvalidMagic(self.magic.get()));
|
||||
}
|
||||
|
||||
if self.major_version.get() != MAJOR_VERSION {
|
||||
return Err(Error::Header(format!(
|
||||
"Unsupported major version: {}",
|
||||
self.major_version.get(),
|
||||
)));
|
||||
return Err(Error::UnsupportedMajorVersion(self.major_version.get()));
|
||||
}
|
||||
|
||||
if self.file_hdr_sz.get() != mem::size_of::<RawHeader>() as u16 {
|
||||
return Err(Error::Header(format!(
|
||||
"Invalid file header size: {}",
|
||||
self.file_hdr_sz.get(),
|
||||
)));
|
||||
} else if self.chunk_hdr_sz.get() != mem::size_of::<RawChunk>() as u16 {
|
||||
return Err(Error::Header(format!(
|
||||
"Invalid chunk header size: {}",
|
||||
self.chunk_hdr_sz.get(),
|
||||
)));
|
||||
if self.file_hdr_sz.get() < mem::size_of::<RawHeader>() as u16 {
|
||||
return Err(Error::InvalidFileHeaderSize(self.file_hdr_sz.get()));
|
||||
} else if self.chunk_hdr_sz.get() < mem::size_of::<RawChunk>() as u16 {
|
||||
return Err(Error::InvalidChunkHeaderSize(self.chunk_hdr_sz.get()));
|
||||
}
|
||||
|
||||
if self.blk_sz.get() == 0 || self.blk_sz.get() % 4 != 0 {
|
||||
return Err(Error::Header(format!(
|
||||
"Invalid block size: {}",
|
||||
self.blk_sz.get(),
|
||||
)));
|
||||
return Err(Error::InvalidBlockSize(self.blk_sz.get()));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn excess_raw_header_bytes(&self) -> u16 {
|
||||
self.file_hdr_sz.get() - mem::size_of::<RawHeader>() as u16
|
||||
}
|
||||
|
||||
fn excess_raw_chunk_bytes(&self) -> u16 {
|
||||
self.chunk_hdr_sz.get() - mem::size_of::<RawChunk>() as u16
|
||||
}
|
||||
}
|
||||
|
||||
/// Raw on-disk layout for the chunk header.
|
||||
#[derive(Clone, Copy, FromZeroes, FromBytes, AsBytes, Unaligned)]
|
||||
#[derive(Clone, Copy, FromBytes, IntoBytes, KnownLayout, Immutable, Unaligned)]
|
||||
#[repr(packed)]
|
||||
struct RawChunk {
|
||||
/// Chunk type. Must be [`CHUNK_TYPE_RAW`], [`CHUNK_TYPE_FILL`],
|
||||
@@ -165,69 +214,58 @@ impl RawChunk {
|
||||
.chunk_sz
|
||||
.get()
|
||||
.checked_mul(header.blk_sz.get())
|
||||
.ok_or_else(|| {
|
||||
Error::Chunk(
|
||||
index,
|
||||
format!(
|
||||
"Chunk size overflow: {} * {}",
|
||||
self.chunk_sz.get(),
|
||||
header.blk_sz.get(),
|
||||
),
|
||||
)
|
||||
.ok_or_else(|| Error::ChunkSizeOverflow {
|
||||
index,
|
||||
chunk_size: self.chunk_sz.get(),
|
||||
block_size: header.blk_sz.get(),
|
||||
})?,
|
||||
CHUNK_TYPE_FILL | CHUNK_TYPE_CRC32 => 4,
|
||||
CHUNK_TYPE_DONT_CARE => 0,
|
||||
t => return Err(Error::Chunk(index, format!("Invalid chunk type: {t}"))),
|
||||
t => {
|
||||
return Err(Error::InvalidChunkType {
|
||||
index,
|
||||
chunk_type: t,
|
||||
})
|
||||
}
|
||||
};
|
||||
|
||||
data_size
|
||||
.checked_add(mem::size_of::<Self>() as u32)
|
||||
.ok_or_else(|| Error::Chunk(index, format!("Data size too large: {data_size}")))
|
||||
.checked_add(header.chunk_hdr_sz.into())
|
||||
.ok_or(Error::DataSizeTooLarge { index, data_size })
|
||||
}
|
||||
|
||||
fn validate(&self, index: u32, header: &RawHeader, start_block: u32) -> Result<()> {
|
||||
let end_block = start_block
|
||||
.checked_add(self.chunk_sz.get())
|
||||
.ok_or_else(|| {
|
||||
Error::Chunk(
|
||||
index,
|
||||
format!(
|
||||
"Block count overflow: {start_block} + {}",
|
||||
self.chunk_sz.get(),
|
||||
),
|
||||
)
|
||||
.ok_or_else(|| Error::BlockCountOverflow {
|
||||
index,
|
||||
start_block,
|
||||
chunk_size: self.chunk_sz.get(),
|
||||
})?;
|
||||
|
||||
if end_block > header.total_blks.get() {
|
||||
return Err(Error::Chunk(
|
||||
return Err(Error::EndBlockExceedsTotal {
|
||||
index,
|
||||
format!(
|
||||
"End block {end_block} exceeds total blocks {}",
|
||||
header.total_blks.get(),
|
||||
),
|
||||
))?;
|
||||
end_block,
|
||||
total_blocks: header.total_blks.get(),
|
||||
})?;
|
||||
}
|
||||
|
||||
if self.chunk_type.get() == CHUNK_TYPE_CRC32 && self.chunk_sz.get() != 0 {
|
||||
return Err(Error::Chunk(
|
||||
return Err(Error::Crc32ChunkNotEmpty {
|
||||
index,
|
||||
format!(
|
||||
"CRC32 chunk has non-zero blocks: {:?}",
|
||||
start_block..end_block,
|
||||
),
|
||||
));
|
||||
chunk_size: self.chunk_sz.get(),
|
||||
});
|
||||
}
|
||||
|
||||
let expected_size = self.expected_size(index, header)?;
|
||||
|
||||
if expected_size != self.total_sz.get() {
|
||||
return Err(Error::Chunk(
|
||||
return Err(Error::InvalidChunkSize {
|
||||
index,
|
||||
format!(
|
||||
"Expected total size {expected_size}, but have {}",
|
||||
self.total_sz.get(),
|
||||
),
|
||||
));
|
||||
expected_size,
|
||||
total_size: self.total_sz.get(),
|
||||
});
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@@ -658,11 +696,15 @@ impl<R: Read> SparseReader<R> {
|
||||
/// data chunks if they are not needed. If the underlying file is seekable
|
||||
/// and skipping chunks is needed, use [`Self::new_seekable`] instead.
|
||||
pub fn new(mut inner: R, crc_mode: CrcMode) -> Result<Self> {
|
||||
let mut header = RawHeader::new_zeroed();
|
||||
inner.read_exact(header.as_bytes_mut())?;
|
||||
let header =
|
||||
RawHeader::read_from_io(&mut inner).map_err(|e| Error::DataRead("header", e))?;
|
||||
|
||||
header.validate()?;
|
||||
|
||||
inner
|
||||
.read_discard(header.excess_raw_header_bytes().into())
|
||||
.map_err(|e| Error::DataRead("header_excess", e))?;
|
||||
|
||||
Ok(Self {
|
||||
inner,
|
||||
seek: None,
|
||||
@@ -701,18 +743,14 @@ impl<R: Read> SparseReader<R> {
|
||||
if self.data_remain != 0 {
|
||||
if let Some(seek) = self.seek {
|
||||
if self.hasher.is_some() {
|
||||
return Err(Error::Reader(
|
||||
"Cannot skip data when CRC validation is enabled".into(),
|
||||
));
|
||||
return Err(Error::Crc32RandomRead);
|
||||
}
|
||||
|
||||
seek(&mut self.inner, SeekFrom::Current(self.data_remain.into()))?;
|
||||
seek(&mut self.inner, SeekFrom::Current(self.data_remain.into()))
|
||||
.map_err(|e| Error::DataRead("data_remain", e))?;
|
||||
self.data_remain = 0;
|
||||
} else {
|
||||
return Err(Error::Reader(format!(
|
||||
"Previous chunk still has {} bytes remaining",
|
||||
self.data_remain,
|
||||
)));
|
||||
return Err(Error::UnreadChunkData(self.data_remain));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -720,11 +758,15 @@ impl<R: Read> SparseReader<R> {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let mut raw_chunk = RawChunk::new_zeroed();
|
||||
self.inner.read_exact(raw_chunk.as_bytes_mut())?;
|
||||
let raw_chunk =
|
||||
RawChunk::read_from_io(&mut self.inner).map_err(|e| Error::DataRead("chunk", e))?;
|
||||
|
||||
raw_chunk.validate(self.chunk, &self.header, self.block)?;
|
||||
|
||||
self.inner
|
||||
.read_discard(self.header.excess_raw_chunk_bytes().into())
|
||||
.map_err(|e| Error::DataRead("chunk_excess", e))?;
|
||||
|
||||
let data: ChunkData;
|
||||
|
||||
match raw_chunk.chunk_type.get() {
|
||||
@@ -735,8 +777,8 @@ impl<R: Read> SparseReader<R> {
|
||||
data = ChunkData::Data;
|
||||
}
|
||||
CHUNK_TYPE_FILL => {
|
||||
let mut fill_value = little_endian::U32::new_zeroed();
|
||||
self.inner.read_exact(fill_value.as_bytes_mut())?;
|
||||
let fill_value = little_endian::U32::read_from_io(&mut self.inner)
|
||||
.map_err(|e| Error::DataRead("chunk_fill_value", e))?;
|
||||
|
||||
if let Some(hasher) = &mut self.hasher {
|
||||
hash_fill_chunk(&raw_chunk, fill_value, &self.header, hasher);
|
||||
@@ -752,16 +794,17 @@ impl<R: Read> SparseReader<R> {
|
||||
data = ChunkData::Hole;
|
||||
}
|
||||
CHUNK_TYPE_CRC32 => {
|
||||
let mut expected = little_endian::U32::new_zeroed();
|
||||
self.inner.read_exact(expected.as_bytes_mut())?;
|
||||
let expected = little_endian::U32::read_from_io(&mut self.inner)
|
||||
.map_err(|e| Error::DataRead("chunk_crc32", e))?;
|
||||
|
||||
if let Some(hasher) = &mut self.hasher {
|
||||
let actual = hasher.clone().finalize();
|
||||
|
||||
if actual != expected.get() {
|
||||
return Err(Error::Reader(format!(
|
||||
"Expected checkpoint CRC32 {expected:08x}, but have {actual:08x}",
|
||||
)));
|
||||
return Err(Error::MismatchedCrc32Checkpoint {
|
||||
expected: expected.get(),
|
||||
actual,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -792,9 +835,7 @@ impl<R: Read> SparseReader<R> {
|
||||
let actual = hasher.finalize();
|
||||
|
||||
if actual != expected {
|
||||
return Err(Error::Reader(format!(
|
||||
"Expected final CRC32 {expected:08x}, but have {actual:08x}",
|
||||
)));
|
||||
return Err(Error::MismatchedCrc32Final { expected, actual });
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -838,10 +879,7 @@ impl<W: Write> SparseWriter<W> {
|
||||
/// file to be seekable, so the [`Header`] must be fully known up front.
|
||||
pub fn new(mut inner: W, header: Header) -> Result<Self> {
|
||||
if header.minor_version != MINOR_VERSION {
|
||||
return Err(Error::Writer(format!(
|
||||
"Minor version not supported for writing: {}",
|
||||
header.minor_version,
|
||||
)));
|
||||
return Err(Error::UnsupportedMinorVersion(header.minor_version));
|
||||
}
|
||||
|
||||
let header = RawHeader {
|
||||
@@ -858,7 +896,9 @@ impl<W: Write> SparseWriter<W> {
|
||||
|
||||
header.validate()?;
|
||||
|
||||
inner.write_all(header.as_bytes())?;
|
||||
header
|
||||
.write_to_io(&mut inner)
|
||||
.map_err(|e| Error::DataWrite("header", e))?;
|
||||
|
||||
Ok(Self {
|
||||
inner,
|
||||
@@ -876,21 +916,18 @@ impl<W: Write> SparseWriter<W> {
|
||||
/// [`ChunkData::Data`], the data must be fully written first.
|
||||
pub fn start_chunk(&mut self, chunk: Chunk) -> Result<()> {
|
||||
if self.data_remain != 0 {
|
||||
return Err(Error::Writer(format!(
|
||||
"Previous chunk still has {} bytes remaining",
|
||||
self.data_remain,
|
||||
)));
|
||||
return Err(Error::UnwrittenChunkData(self.data_remain));
|
||||
}
|
||||
|
||||
if self.chunk == self.header.total_chunks.get() {
|
||||
return Err(Error::Writer("Already wrote all chunk headers".into()));
|
||||
return Err(Error::TooManyChunks);
|
||||
}
|
||||
|
||||
if chunk.bounds.start != self.block {
|
||||
return Err(Error::Writer(format!(
|
||||
"Gap between end of last chunk {} and start of new chunk {}",
|
||||
self.block, chunk.bounds.start,
|
||||
)));
|
||||
return Err(Error::GapBetweenChunks {
|
||||
prev_end: self.block,
|
||||
cur_start: chunk.bounds.start,
|
||||
});
|
||||
}
|
||||
|
||||
let mut raw_chunk = RawChunk {
|
||||
@@ -912,7 +949,9 @@ impl<W: Write> SparseWriter<W> {
|
||||
self.chunk += 1;
|
||||
self.block = chunk.bounds.end;
|
||||
|
||||
self.inner.write_all(raw_chunk.as_bytes())?;
|
||||
raw_chunk
|
||||
.write_to_io(&mut self.inner)
|
||||
.map_err(|e| Error::DataWrite("chunk", e))?;
|
||||
|
||||
match chunk.data {
|
||||
ChunkData::Data => {
|
||||
@@ -920,7 +959,9 @@ impl<W: Write> SparseWriter<W> {
|
||||
raw_chunk.total_sz.get() - u32::from(self.header.chunk_hdr_sz.get());
|
||||
}
|
||||
ChunkData::Fill(fill_value) => {
|
||||
self.inner.write_all(&fill_value.to_le_bytes())?;
|
||||
self.inner
|
||||
.write_all(&fill_value.to_le_bytes())
|
||||
.map_err(|e| Error::DataWrite("chunk_fill_value", e))?;
|
||||
|
||||
hash_fill_chunk(
|
||||
&raw_chunk,
|
||||
@@ -933,13 +974,13 @@ impl<W: Write> SparseWriter<W> {
|
||||
hash_fill_chunk(&raw_chunk, 0.into(), &self.header, &mut self.hasher);
|
||||
}
|
||||
ChunkData::Crc32(expected) => {
|
||||
self.inner.write_all(&expected.to_le_bytes())?;
|
||||
self.inner
|
||||
.write_all(&expected.to_le_bytes())
|
||||
.map_err(|e| Error::DataWrite("chunk_crc32", e))?;
|
||||
|
||||
let actual = self.hasher.clone().finalize();
|
||||
if actual != expected {
|
||||
return Err(Error::Reader(format!(
|
||||
"Expected checkpoint CRC32 {expected:08x}, but have {actual:08x}",
|
||||
)));
|
||||
return Err(Error::MismatchedCrc32Checkpoint { expected, actual });
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -954,9 +995,7 @@ impl<W: Write> SparseWriter<W> {
|
||||
let actual = self.hasher.finalize();
|
||||
|
||||
if actual != expected {
|
||||
return Err(Error::Reader(format!(
|
||||
"Expected final CRC32 {expected:08x}, but have {actual:08x}",
|
||||
)));
|
||||
return Err(Error::MismatchedCrc32Final { expected, actual });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2023 Andrew Gunnerson
|
||||
* SPDX-License-Identifier: GPL-3.0-only
|
||||
*/
|
||||
// SPDX-FileCopyrightText: 2023 Andrew Gunnerson
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
// The gf256 library uses compile-time proc macro code generation. Since
|
||||
// dm-verity supports RS(255, 231) through RS(255, 253), we'll generate RS
|
||||
|
||||
+2
-4
@@ -1,7 +1,5 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2023 Andrew Gunnerson
|
||||
* SPDX-License-Identifier: GPL-3.0-only
|
||||
*/
|
||||
// SPDX-FileCopyrightText: 2023 Andrew Gunnerson
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
//! Since avbroot is primarily an application and not a library, the semver
|
||||
//! versioning covers the CLI only. All Rust APIs can change at any time, even
|
||||
|
||||
+3
-5
@@ -1,7 +1,5 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2023 Andrew Gunnerson
|
||||
* SPDX-License-Identifier: GPL-3.0-only
|
||||
*/
|
||||
// SPDX-FileCopyrightText: 2023 Andrew Gunnerson
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use std::{
|
||||
process::ExitCode,
|
||||
@@ -28,7 +26,7 @@ fn main() -> ExitCode {
|
||||
}
|
||||
|
||||
match avbroot::cli::args::main(&LOGGING_INITIALIZED, &cancel_signal) {
|
||||
Ok(_) => ExitCode::SUCCESS,
|
||||
Ok(()) => ExitCode::SUCCESS,
|
||||
Err(e) => {
|
||||
if LOGGING_INITIALIZED.load(Ordering::SeqCst) {
|
||||
error!("{e:?}");
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2023 Andrew Gunnerson
|
||||
* SPDX-License-Identifier: GPL-3.0-only
|
||||
*/
|
||||
// SPDX-FileCopyrightText: 2023 Andrew Gunnerson
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
//! Hack to format an integer as an octal string because toml_edit can't output
|
||||
//! octal-formatted integers and many other toml parsers can't parse it either.
|
||||
@@ -30,7 +28,7 @@ where
|
||||
{
|
||||
struct OctalStrVisitor<T>(PhantomData<T>);
|
||||
|
||||
impl<'de, T> Visitor<'de> for OctalStrVisitor<T>
|
||||
impl<T> Visitor<'_> for OctalStrVisitor<T>
|
||||
where
|
||||
T: PrimInt,
|
||||
<T as Num>::FromStrRadixErr: fmt::Display,
|
||||
|
||||
+243
-159
@@ -1,20 +1,20 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2022-2024 Andrew Gunnerson
|
||||
* SPDX-License-Identifier: GPL-3.0-only
|
||||
*/
|
||||
// SPDX-FileCopyrightText: 2022-2024 Andrew Gunnerson
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use std::{
|
||||
cmp::Ordering,
|
||||
collections::{HashMap, HashSet},
|
||||
fmt::Write,
|
||||
fs::File,
|
||||
io::{self, BufRead, BufReader, Cursor, Read, Seek},
|
||||
io::{self, BufRead, BufReader, Cursor, Read},
|
||||
num::ParseIntError,
|
||||
ops::Range,
|
||||
ops::{Range, RangeFrom},
|
||||
path::{Path, PathBuf},
|
||||
slice,
|
||||
slice, str,
|
||||
sync::atomic::AtomicBool,
|
||||
};
|
||||
|
||||
use aws_lc_rs::digest::Context;
|
||||
use bstr::ByteSlice;
|
||||
use liblzma::{
|
||||
stream::{Check, Stream},
|
||||
@@ -22,7 +22,6 @@ use liblzma::{
|
||||
};
|
||||
use rayon::iter::{IntoParallelRefIterator, IntoParallelRefMutIterator, ParallelIterator};
|
||||
use regex::bytes::Regex;
|
||||
use ring::digest::Context;
|
||||
use rsa::RsaPublicKey;
|
||||
use thiserror::Error;
|
||||
use tracing::{debug, debug_span, trace, warn, Span};
|
||||
@@ -43,8 +42,6 @@ use crate::{
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum Error {
|
||||
#[error("No compatible boot image found for {0}")]
|
||||
NoTargets(&'static str),
|
||||
#[error("Boot image has no vbmeta footer")]
|
||||
NoFooter,
|
||||
#[error("No hash descriptor found in vbmeta header")]
|
||||
@@ -55,41 +52,80 @@ pub enum Error {
|
||||
ParseMagiskVersion(String, #[source] ParseIntError),
|
||||
#[error("Failed to determine Magisk version from: {0:?}")]
|
||||
FindMagiskVersion(PathBuf),
|
||||
#[error("AVB error")]
|
||||
Avb(#[from] avb::Error),
|
||||
#[error("Boot image error")]
|
||||
BootImage(#[from] bootimage::Error),
|
||||
#[error("Compression error")]
|
||||
Compression(#[from] compression::Error),
|
||||
#[error("Crypto error")]
|
||||
Crypto(#[from] crypto::Error),
|
||||
#[error("CPIO error")]
|
||||
Cpio(#[from] cpio::Error),
|
||||
#[error("OTA certificate error")]
|
||||
OtaCert(#[from] otacert::Error),
|
||||
#[error("XZ stream error")]
|
||||
XzStream(#[from] liblzma::stream::Error),
|
||||
#[error("Zip error")]
|
||||
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")]
|
||||
File(PathBuf, #[source] io::Error),
|
||||
#[error("Failed to load potentially compressed ramdisk")]
|
||||
RamdiskLoadCompression(#[source] compression::Error),
|
||||
#[error("Failed to save potentially compressed ramdisk")]
|
||||
RamdiskSaveCompression(#[source] compression::Error),
|
||||
#[error("Failed to finalize compressed ramdisk")]
|
||||
RamdiskSaveCompressionFinalize(#[source] io::Error),
|
||||
#[error("Failed to load ramdisk cpio entries")]
|
||||
RamdiskLoadCpio(#[source] cpio::Error),
|
||||
#[error("Failed to save ramdisk cpio entries")]
|
||||
RamdiskSaveCpio(#[source] cpio::Error),
|
||||
#[error("Failed to load potentially compressed kernel")]
|
||||
KernelLoadCompression(#[source] compression::Error),
|
||||
#[error("Failed to read kernel image")]
|
||||
KernelRead(#[source] io::Error),
|
||||
#[error("Failed to load boot image")]
|
||||
BootImageLoad(#[source] bootimage::Error),
|
||||
#[error("Failed to save boot image")]
|
||||
BootImageSave(#[source] bootimage::Error),
|
||||
#[error("Failed to seek boot image")]
|
||||
BootImageSeek(#[source] io::Error),
|
||||
#[error("Failed to encode public key in AVB binary format")]
|
||||
AvbEncodeKey(#[source] avb::Error),
|
||||
#[error("Failed to load AVB header from boot image")]
|
||||
AvbLoad(#[source] avb::Error),
|
||||
#[error("Failed to update AVB header for boot image")]
|
||||
AvbUpdate(#[source] avb::Error),
|
||||
#[error("Failed to load OTA certificate")]
|
||||
OtaCertLoad(#[source] crypto::Error),
|
||||
#[error("Failed to generate replacement otacerts zip")]
|
||||
OtaCertZip(#[source] otacert::Error),
|
||||
#[error("Failed to initialize XZ encoder")]
|
||||
XzInit(#[source] liblzma::stream::Error),
|
||||
#[error("Failed to XZ compress entry: {:?}", .0.as_bstr())]
|
||||
XzCompress(Vec<u8>, #[source] io::Error),
|
||||
#[error("Failed to open zip file: {0:?}")]
|
||||
ZipOpen(PathBuf, #[source] ZipError),
|
||||
#[error("Failed to open zip entry: {0:?}")]
|
||||
ZipEntryOpen(&'static str, #[source] ZipError),
|
||||
#[error("Failed to read zip entry: {0:?}")]
|
||||
ZipEntryRead(&'static str, #[source] io::Error),
|
||||
#[error("Failed to open zip entry #{0}")]
|
||||
ZipIndexOpen(usize, #[source] ZipError),
|
||||
#[error("Failed to open file: {0:?}")]
|
||||
FileOpen(PathBuf, #[source] io::Error),
|
||||
}
|
||||
|
||||
type Result<T> = std::result::Result<T, Error>;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum TargetsError {
|
||||
#[error("No compatible boot image found for {0}")]
|
||||
NoTargets(&'static str),
|
||||
#[error("Targets validation error: {0}")]
|
||||
TargetValidation(String),
|
||||
#[error("Failed to open boot image: {0}")]
|
||||
Open(String, #[source] io::Error),
|
||||
#[error("Failed to load boot image: {0}")]
|
||||
Load(String, #[source] Error),
|
||||
#[error("Failed to save boot image: {0}")]
|
||||
Save(String, #[source] Error),
|
||||
#[error("Failed to patch boot image: {0}")]
|
||||
Patch(String, #[source] Error),
|
||||
}
|
||||
|
||||
type TargetsResult<T> = std::result::Result<T, TargetsError>;
|
||||
|
||||
fn load_ramdisk(
|
||||
data: &[u8],
|
||||
cancel_signal: &AtomicBool,
|
||||
) -> Result<(Vec<CpioEntry>, CompressedFormat)> {
|
||||
let raw_reader = Cursor::new(data);
|
||||
let mut reader = CompressedReader::new(raw_reader, false)?;
|
||||
let entries = cpio::load(&mut reader, false, cancel_signal)?;
|
||||
let mut reader =
|
||||
CompressedReader::new(raw_reader, false).map_err(Error::RamdiskLoadCompression)?;
|
||||
let entries = cpio::load(&mut reader, false, cancel_signal).map_err(Error::RamdiskLoadCpio)?;
|
||||
|
||||
trace!(
|
||||
"Loaded {:?} ramdisk with {} entries",
|
||||
@@ -106,12 +142,15 @@ fn save_ramdisk(
|
||||
cancel_signal: &AtomicBool,
|
||||
) -> Result<Vec<u8>> {
|
||||
let raw_writer = Cursor::new(vec![]);
|
||||
let mut writer = CompressedWriter::new(raw_writer, format)?;
|
||||
cpio::save(&mut writer, entries, false, cancel_signal)?;
|
||||
let mut writer =
|
||||
CompressedWriter::new(raw_writer, format).map_err(Error::RamdiskSaveCompression)?;
|
||||
cpio::save(&mut writer, entries, false, cancel_signal).map_err(Error::RamdiskSaveCpio)?;
|
||||
|
||||
trace!("Wrote {format:?} ramdisk with {} entries", entries.len());
|
||||
|
||||
let raw_writer = writer.finish()?;
|
||||
let raw_writer = writer
|
||||
.finish()
|
||||
.map_err(Error::RamdiskSaveCompressionFinalize)?;
|
||||
Ok(raw_writer.into_inner())
|
||||
}
|
||||
|
||||
@@ -132,7 +171,7 @@ pub trait BootImagePatch {
|
||||
&self,
|
||||
boot_images: &HashMap<&'a str, BootImageInfo>,
|
||||
cancel_signal: &AtomicBool,
|
||||
) -> Result<Vec<&'a str>>;
|
||||
) -> TargetsResult<Vec<&'a str>>;
|
||||
|
||||
fn patch(&self, boot_image: &mut BootImage, cancel_signal: &AtomicBool) -> Result<()>;
|
||||
}
|
||||
@@ -153,13 +192,13 @@ impl MagiskRootPatcher {
|
||||
// RULESDEVICE config option, which stored the writable block device as an
|
||||
// rdev major/minor pair, which was not consistent across reboots and was
|
||||
// replaced by PREINITDEVICE
|
||||
const VERS_SUPPORTED: &'static [Range<u32>] = &[25102..25207, 25211..27100];
|
||||
const VER_PREINIT_DEVICE: Range<u32> =
|
||||
25211..Self::VERS_SUPPORTED[Self::VERS_SUPPORTED.len() - 1].end;
|
||||
// - Versions newer than the latest supported version are assumed to support
|
||||
// the same features as the latest version
|
||||
const VERS_SUPPORTED: &'static [Range<u32>] = &[25102..25207, 25211..28200];
|
||||
const VER_PREINIT_DEVICE: RangeFrom<u32> = 25211..;
|
||||
const VER_RANDOM_SEED: Range<u32> = 25211..26103;
|
||||
const VER_PATCH_VBMETA: Range<u32> = Self::VERS_SUPPORTED[0].start..26202;
|
||||
const VER_XZ_BACKUP: Range<u32> =
|
||||
26403..Self::VERS_SUPPORTED[Self::VERS_SUPPORTED.len() - 1].end;
|
||||
const VER_XZ_BACKUP: RangeFrom<u32> = 26403..;
|
||||
|
||||
const ZIP_INIT_LD: &'static str = "lib/arm64-v8a/libinit-ld.so";
|
||||
const ZIP_LIBMAGISK: &'static str = "lib/arm64-v8a/libmagisk.so";
|
||||
@@ -218,18 +257,21 @@ 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).map_err(Error::Zip)?;
|
||||
let reader = File::open(path)
|
||||
.map(BufReader::new)
|
||||
.map_err(|e| Error::FileOpen(path.to_owned(), e))?;
|
||||
let mut zip = ZipArchive::new(reader).map_err(|e| Error::ZipOpen(path.to_owned(), e))?;
|
||||
let entry = zip
|
||||
.by_name(Self::ZIP_UTIL_FUNCTIONS)
|
||||
.map_err(|e| Error::ZipEntryName(Self::ZIP_UTIL_FUNCTIONS.to_owned(), e))?;
|
||||
.map_err(|e| Error::ZipEntryOpen(Self::ZIP_UTIL_FUNCTIONS, e))?;
|
||||
let mut entry = BufReader::new(entry);
|
||||
let mut line = String::new();
|
||||
|
||||
loop {
|
||||
line.clear();
|
||||
let n = entry.read_line(&mut line)?;
|
||||
let n = entry
|
||||
.read_line(&mut line)
|
||||
.map_err(|e| Error::ZipEntryRead(Self::ZIP_UTIL_FUNCTIONS, e))?;
|
||||
if n == 0 {
|
||||
return Err(Error::FindMagiskVersion(path.to_owned()));
|
||||
}
|
||||
@@ -245,14 +287,14 @@ impl MagiskRootPatcher {
|
||||
}
|
||||
}
|
||||
|
||||
fn xz_compress(reader: impl Read, cancel_signal: &AtomicBool) -> Result<Vec<u8>> {
|
||||
let stream = Stream::new_easy_encoder(9, Check::Crc32)?;
|
||||
fn xz_compress(name: &[u8], reader: impl Read, cancel_signal: &AtomicBool) -> Result<Vec<u8>> {
|
||||
let stream = Stream::new_easy_encoder(9, Check::Crc32).map_err(Error::XzInit)?;
|
||||
let raw_writer = Cursor::new(Vec::new());
|
||||
let mut writer = XzEncoder::new_stream(raw_writer, stream);
|
||||
|
||||
stream::copy(reader, &mut writer, cancel_signal)?;
|
||||
|
||||
let raw_writer = writer.finish()?;
|
||||
let raw_writer = stream::copy(reader, &mut writer, cancel_signal)
|
||||
.and_then(|_| writer.finish())
|
||||
.map_err(|e| Error::XzCompress(name.to_owned(), e))?;
|
||||
|
||||
Ok(raw_writer.into_inner())
|
||||
}
|
||||
@@ -341,7 +383,7 @@ impl MagiskRootPatcher {
|
||||
new_path.extend(b".xz");
|
||||
|
||||
let reader = Cursor::new(data);
|
||||
let buf = Self::xz_compress(reader, cancel_signal)?;
|
||||
let buf = Self::xz_compress(&new_path, reader, cancel_signal)?;
|
||||
new_data = Some(CpioEntryData::Data(buf));
|
||||
}
|
||||
}
|
||||
@@ -383,7 +425,7 @@ impl BootImagePatch for MagiskRootPatcher {
|
||||
&self,
|
||||
boot_images: &HashMap<&'a str, BootImageInfo>,
|
||||
_cancel_signal: &AtomicBool,
|
||||
) -> Result<Vec<&'a str>> {
|
||||
) -> TargetsResult<Vec<&'a str>> {
|
||||
let mut targets = vec![];
|
||||
|
||||
if boot_images.contains_key("init_boot") {
|
||||
@@ -396,9 +438,11 @@ 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)).map_err(Error::Zip)?;
|
||||
let zip_reader = File::open(&self.apk_path)
|
||||
.map(BufReader::new)
|
||||
.map_err(|e| Error::FileOpen(self.apk_path.clone(), e))?;
|
||||
let mut zip =
|
||||
ZipArchive::new(zip_reader).map_err(|e| Error::ZipOpen(self.apk_path.clone(), e))?;
|
||||
|
||||
// Load the first ramdisk. If it doesn't exist, we have to generate one
|
||||
// from scratch.
|
||||
@@ -429,9 +473,11 @@ impl BootImagePatch for MagiskRootPatcher {
|
||||
{
|
||||
let mut zip_entry = zip
|
||||
.by_name(Self::ZIP_MAGISKINIT)
|
||||
.map_err(|e| Error::ZipEntryName(Self::ZIP_MAGISKINIT.to_owned(), e))?;
|
||||
.map_err(|e| Error::ZipEntryOpen(Self::ZIP_MAGISKINIT, e))?;
|
||||
let mut data = vec![];
|
||||
zip_entry.read_to_end(&mut data)?;
|
||||
zip_entry
|
||||
.read_to_end(&mut data)
|
||||
.map_err(|e| Error::ZipEntryRead(Self::ZIP_MAGISKINIT, e))?;
|
||||
|
||||
entries.push(CpioEntry::new_file(
|
||||
b"init",
|
||||
@@ -473,8 +519,8 @@ impl BootImagePatch for MagiskRootPatcher {
|
||||
for (source, target) in xz_files {
|
||||
let reader = zip
|
||||
.by_name(source)
|
||||
.map_err(|e| Error::ZipEntryName(source.to_owned(), e))?;
|
||||
let buf = Self::xz_compress(reader, cancel_signal)?;
|
||||
.map_err(|e| Error::ZipEntryOpen(source, e))?;
|
||||
let buf = Self::xz_compress(source.as_bytes(), reader, cancel_signal)?;
|
||||
|
||||
entries.push(CpioEntry::new_file(target, 0o644, CpioEntryData::Data(buf)));
|
||||
}
|
||||
@@ -499,10 +545,9 @@ impl BootImagePatch for MagiskRootPatcher {
|
||||
magisk_config.push_str("RECOVERYMODE=false\n");
|
||||
|
||||
if Self::VER_PREINIT_DEVICE.contains(&self.version) {
|
||||
magisk_config.push_str(&format!(
|
||||
"PREINITDEVICE={}\n",
|
||||
self.preinit_device.as_ref().unwrap(),
|
||||
));
|
||||
if let Some(device) = &self.preinit_device {
|
||||
writeln!(&mut magisk_config, "PREINITDEVICE={device}").unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
// Magisk normally saves the original SHA1 digest in its config file. It
|
||||
@@ -512,7 +557,7 @@ impl BootImagePatch for MagiskRootPatcher {
|
||||
magisk_config.push_str("SHA1=0000000000000000000000000000000000000000\n");
|
||||
|
||||
if Self::VER_RANDOM_SEED.contains(&self.version) {
|
||||
magisk_config.push_str(&format!("RANDOMSEED={:#x}\n", self.random_seed));
|
||||
writeln!(&mut magisk_config, "RANDOMSEED={:#x}", self.random_seed).unwrap();
|
||||
}
|
||||
|
||||
trace!("Magisk config: {magisk_config:?}");
|
||||
@@ -525,7 +570,7 @@ impl BootImagePatch for MagiskRootPatcher {
|
||||
|
||||
// Repack ramdisk.
|
||||
cpio::sort(&mut entries);
|
||||
cpio::assign_inodes(&mut entries, false)?;
|
||||
cpio::assign_inodes(&mut entries, false).map_err(Error::RamdiskSaveCpio)?;
|
||||
let new_ramdisk = save_ramdisk(&entries, ramdisk_format, cancel_signal)?;
|
||||
|
||||
match boot_image {
|
||||
@@ -592,18 +637,22 @@ impl OtaCertPatcher {
|
||||
continue;
|
||||
};
|
||||
|
||||
let mut zip = ZipArchive::new(Cursor::new(&data)).map_err(Error::Zip)?;
|
||||
let mut zip = ZipArchive::new(Cursor::new(&data)).map_err(|e| {
|
||||
Error::ZipOpen(str::from_utf8(Self::OTACERTS_PATH).unwrap().into(), e)
|
||||
})?;
|
||||
|
||||
for index in 0..zip.len() {
|
||||
let zip_entry = zip
|
||||
.by_index(index)
|
||||
.map_err(|e| Error::ZipEntryIndex(index, e))?;
|
||||
.map_err(|e| Error::ZipIndexOpen(index, e))?;
|
||||
if !zip_entry.name().ends_with(".x509.pem") {
|
||||
debug!("Skipping invalid entry path: {}", zip_entry.name());
|
||||
continue;
|
||||
}
|
||||
|
||||
let certificate = crypto::read_pem_cert(zip_entry)?;
|
||||
let path = PathBuf::from(zip_entry.name());
|
||||
let certificate =
|
||||
crypto::read_pem_cert(&path, zip_entry).map_err(Error::OtaCertLoad)?;
|
||||
certificates.push(certificate);
|
||||
}
|
||||
}
|
||||
@@ -612,7 +661,6 @@ impl OtaCertPatcher {
|
||||
}
|
||||
|
||||
fn patch_ramdisk(
|
||||
&self,
|
||||
ramdisk: &mut Vec<u8>,
|
||||
zip: &[u8],
|
||||
cancel_signal: &AtomicBool,
|
||||
@@ -642,10 +690,10 @@ impl BootImagePatch for OtaCertPatcher {
|
||||
&self,
|
||||
boot_images: &HashMap<&'a str, BootImageInfo>,
|
||||
cancel_signal: &AtomicBool,
|
||||
) -> Result<Vec<&'a str>> {
|
||||
) -> TargetsResult<Vec<&'a str>> {
|
||||
let mut targets = vec![];
|
||||
|
||||
'outer: for (name, info) in boot_images {
|
||||
'outer: for (&name, info) in boot_images {
|
||||
let ramdisks = match &info.boot_image {
|
||||
BootImage::V0Through2(b) => slice::from_ref(&b.ramdisk),
|
||||
BootImage::V3Through4(b) => slice::from_ref(&b.ramdisk),
|
||||
@@ -657,9 +705,10 @@ impl BootImagePatch for OtaCertPatcher {
|
||||
continue;
|
||||
}
|
||||
|
||||
let (entries, _) = load_ramdisk(ramdisk, cancel_signal)?;
|
||||
let (entries, _) = load_ramdisk(ramdisk, cancel_signal)
|
||||
.map_err(|e| TargetsError::Load(name.to_owned(), e))?;
|
||||
if entries.iter().any(|e| e.path == Self::OTACERTS_PATH) {
|
||||
targets.push(*name);
|
||||
targets.push(name);
|
||||
continue 'outer;
|
||||
}
|
||||
}
|
||||
@@ -675,7 +724,8 @@ impl BootImagePatch for OtaCertPatcher {
|
||||
BootImage::VendorV3Through4(b) => &mut b.ramdisks,
|
||||
};
|
||||
|
||||
let new_zip = otacert::create_zip(&self.cert, OtaCertBuildFlags::empty())?;
|
||||
let new_zip = otacert::create_zip(&self.cert, OtaCertBuildFlags::empty())
|
||||
.map_err(Error::OtaCertZip)?;
|
||||
trace!("Generated new {} byte otacerts.zip", new_zip.len());
|
||||
|
||||
for ramdisk in ramdisks {
|
||||
@@ -683,7 +733,7 @@ impl BootImagePatch for OtaCertPatcher {
|
||||
continue;
|
||||
}
|
||||
|
||||
if self.patch_ramdisk(ramdisk, &new_zip, cancel_signal)? {
|
||||
if Self::patch_ramdisk(ramdisk, &new_zip, cancel_signal)? {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
@@ -721,7 +771,8 @@ impl DsuPubKeyPatcher {
|
||||
entries.push(CpioEntry::new_directory(Self::DSU_KEYS_PATH, 0o755));
|
||||
}
|
||||
|
||||
let data = CpioEntryData::Data(avb::encode_public_key(&self.key)?);
|
||||
let binary_key = avb::encode_public_key(&self.key).map_err(Error::AvbEncodeKey)?;
|
||||
let data = CpioEntryData::Data(binary_key);
|
||||
|
||||
if let Some(e) = entries
|
||||
.iter_mut()
|
||||
@@ -747,11 +798,11 @@ impl BootImagePatch for DsuPubKeyPatcher {
|
||||
&self,
|
||||
boot_images: &HashMap<&'a str, BootImageInfo>,
|
||||
cancel_signal: &AtomicBool,
|
||||
) -> Result<Vec<&'a str>> {
|
||||
) -> TargetsResult<Vec<&'a str>> {
|
||||
let mut dsu_keys_targets = vec![];
|
||||
let mut first_stage_targets = vec![];
|
||||
|
||||
'outer: for (name, info) in boot_images {
|
||||
'outer: for (&name, info) in boot_images {
|
||||
let ramdisks = match &info.boot_image {
|
||||
BootImage::V0Through2(b) => slice::from_ref(&b.ramdisk),
|
||||
BootImage::V3Through4(b) => slice::from_ref(&b.ramdisk),
|
||||
@@ -763,15 +814,16 @@ impl BootImagePatch for DsuPubKeyPatcher {
|
||||
continue;
|
||||
}
|
||||
|
||||
let (entries, _) = load_ramdisk(ramdisk, cancel_signal)?;
|
||||
let (entries, _) = load_ramdisk(ramdisk, cancel_signal)
|
||||
.map_err(|e| TargetsError::Load(name.to_owned(), e))?;
|
||||
let mut found = false;
|
||||
|
||||
for entry in entries {
|
||||
if entry.path == Self::DSU_KEYS_PATH {
|
||||
dsu_keys_targets.push(*name);
|
||||
dsu_keys_targets.push(name);
|
||||
found = true;
|
||||
} else if entry.path == Self::FIRST_STAGE_PATH {
|
||||
first_stage_targets.push(*name);
|
||||
first_stage_targets.push(name);
|
||||
found = true;
|
||||
}
|
||||
}
|
||||
@@ -786,7 +838,7 @@ impl BootImagePatch for DsuPubKeyPatcher {
|
||||
// Most builds trust as least one DSU key. For these builds, add the
|
||||
// user's key to the same directory.
|
||||
if dsu_keys_targets.len() > 1 {
|
||||
return Err(Error::Validation(format!(
|
||||
return Err(TargetsError::TargetValidation(format!(
|
||||
"DSU keys found in more than one boot image: {dsu_keys_targets:?}",
|
||||
)));
|
||||
}
|
||||
@@ -796,7 +848,7 @@ impl BootImagePatch for DsuPubKeyPatcher {
|
||||
// For builds that don't trust any DSU keys, pick the first boot
|
||||
// image that contains a first stage ramdisk directory.
|
||||
if !first_stage_targets.is_empty() {
|
||||
first_stage_targets.sort();
|
||||
first_stage_targets.sort_unstable();
|
||||
first_stage_targets.resize(1, "");
|
||||
}
|
||||
|
||||
@@ -855,19 +907,22 @@ impl PrepatchedImagePatcher {
|
||||
}
|
||||
|
||||
fn load_prepatched_image(&self) -> Result<BootImage> {
|
||||
let raw_reader =
|
||||
File::open(&self.prepatched).map_err(|e| Error::File(self.prepatched.clone(), e))?;
|
||||
let boot_image = BootImage::from_reader(BufReader::new(raw_reader))?;
|
||||
let reader = File::open(&self.prepatched)
|
||||
.map(BufReader::new)
|
||||
.map_err(|e| Error::FileOpen(self.prepatched.clone(), e))?;
|
||||
|
||||
Ok(boot_image)
|
||||
BootImage::from_reader(reader).map_err(Error::BootImageLoad)
|
||||
}
|
||||
|
||||
fn get_kmi_version(kernel: &[u8]) -> Result<Option<String>> {
|
||||
let mut decompressed = vec![];
|
||||
{
|
||||
let raw_reader = Cursor::new(kernel);
|
||||
let mut reader = CompressedReader::new(raw_reader, true)?;
|
||||
reader.read_to_end(&mut decompressed)?;
|
||||
let mut reader =
|
||||
CompressedReader::new(raw_reader, true).map_err(Error::KernelLoadCompression)?;
|
||||
reader
|
||||
.read_to_end(&mut decompressed)
|
||||
.map_err(Error::KernelRead)?;
|
||||
}
|
||||
|
||||
let regex = Regex::new(Self::VERSION_REGEX).unwrap();
|
||||
@@ -899,8 +954,10 @@ impl BootImagePatch for PrepatchedImagePatcher {
|
||||
&self,
|
||||
boot_images: &HashMap<&'a str, BootImageInfo>,
|
||||
_cancel_signal: &AtomicBool,
|
||||
) -> Result<Vec<&'a str>> {
|
||||
let prepatched_image = self.load_prepatched_image()?;
|
||||
) -> TargetsResult<Vec<&'a str>> {
|
||||
let prepatched_image = self
|
||||
.load_prepatched_image()
|
||||
.map_err(|e| TargetsError::Load("prepatched".to_owned(), e))?;
|
||||
|
||||
let has_kernel = match prepatched_image {
|
||||
BootImage::V0Through2(b) => !b.kernel.is_empty(),
|
||||
@@ -1087,36 +1144,90 @@ impl BootImagePatch for PrepatchedImagePatcher {
|
||||
}
|
||||
}
|
||||
|
||||
fn load_boot_image(reader: &mut dyn ReadSeek) -> Result<BootImageInfo> {
|
||||
let (header, footer, image_size) = avb::load_image(&mut *reader).map_err(Error::AvbLoad)?;
|
||||
let Some(footer) = footer else {
|
||||
return Err(Error::NoFooter);
|
||||
};
|
||||
|
||||
let section_reader =
|
||||
SectionReader::new(reader, 0, footer.original_image_size).map_err(Error::BootImageSeek)?;
|
||||
let boot_image = BootImage::from_reader(section_reader).map_err(Error::BootImageLoad)?;
|
||||
|
||||
let info = BootImageInfo {
|
||||
header,
|
||||
footer,
|
||||
image_size,
|
||||
boot_image,
|
||||
};
|
||||
|
||||
trace!("Loaded {image_size} byte boot image");
|
||||
|
||||
Ok(info)
|
||||
}
|
||||
|
||||
fn save_boot_image(
|
||||
writer: &mut dyn WriteSeek,
|
||||
info: &mut BootImageInfo,
|
||||
key: &RsaSigningKey,
|
||||
) -> Result<()> {
|
||||
let AppendedDescriptorMut::Hash(descriptor) = info
|
||||
.header
|
||||
.appended_descriptor_mut()
|
||||
.map_err(Error::AvbUpdate)?
|
||||
else {
|
||||
return Err(Error::NoHashDescriptor);
|
||||
};
|
||||
|
||||
// Write new boot image. We reuse the existing salt for the digest.
|
||||
let mut context = Context::new(&aws_lc_rs::digest::SHA256);
|
||||
context.update(&descriptor.salt);
|
||||
let mut hashing_writer = HashingWriter::new(writer, context);
|
||||
info.boot_image
|
||||
.to_writer(&mut hashing_writer)
|
||||
.map_err(Error::BootImageSave)?;
|
||||
let (writer, context) = hashing_writer.finish();
|
||||
|
||||
descriptor.image_size = writer.stream_position().map_err(Error::BootImageSeek)?;
|
||||
"sha256".clone_into(&mut descriptor.hash_algorithm);
|
||||
descriptor.root_digest = context.finish().as_ref().to_vec();
|
||||
|
||||
if !info.header.public_key.is_empty() {
|
||||
debug!("Signing boot image");
|
||||
info.header
|
||||
.set_algo_for_key(key)
|
||||
.map_err(Error::AvbUpdate)?;
|
||||
info.header.sign(key).map_err(Error::AvbUpdate)?;
|
||||
}
|
||||
|
||||
avb::write_appended_image(
|
||||
writer,
|
||||
&info.header,
|
||||
&mut info.footer,
|
||||
Some(info.image_size),
|
||||
)
|
||||
.map_err(Error::AvbUpdate)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn load_boot_images<'a>(
|
||||
names: &[&'a str],
|
||||
open_input: impl Fn(&str) -> io::Result<Box<dyn ReadSeek>> + Sync,
|
||||
) -> Result<HashMap<&'a str, BootImageInfo>> {
|
||||
) -> TargetsResult<HashMap<&'a str, BootImageInfo>> {
|
||||
let parent_span = Span::current();
|
||||
|
||||
names
|
||||
.par_iter()
|
||||
.map(|name| {
|
||||
.map(|&name| {
|
||||
let _span = debug_span!(parent: &parent_span, "image", name).entered();
|
||||
let mut reader = open_input(name)?;
|
||||
let mut reader =
|
||||
open_input(name).map_err(|e| TargetsError::Open(name.to_owned(), e))?;
|
||||
|
||||
let (header, footer, image_size) = avb::load_image(&mut reader)?;
|
||||
let Some(footer) = footer else {
|
||||
return Err(Error::NoFooter);
|
||||
};
|
||||
let info =
|
||||
load_boot_image(&mut reader).map_err(|e| TargetsError::Load(name.to_owned(), e))?;
|
||||
|
||||
let section_reader = SectionReader::new(reader, 0, footer.original_image_size)?;
|
||||
let boot_image = BootImage::from_reader(section_reader)?;
|
||||
|
||||
let info = BootImageInfo {
|
||||
header,
|
||||
footer,
|
||||
image_size,
|
||||
boot_image,
|
||||
};
|
||||
|
||||
trace!("Loaded {image_size} byte boot image: {name}");
|
||||
|
||||
Ok((*name, info))
|
||||
Ok((name, info))
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
@@ -1133,9 +1244,14 @@ pub fn patch_boot_images<'a>(
|
||||
key: &RsaSigningKey,
|
||||
patchers: &[Box<dyn BootImagePatch + Sync>],
|
||||
cancel_signal: &AtomicBool,
|
||||
) -> Result<HashSet<&'a str>> {
|
||||
) -> TargetsResult<HashSet<&'a str>> {
|
||||
let parent_span = Span::current();
|
||||
|
||||
if patchers.is_empty() {
|
||||
debug!("Skip loading boot images; nothing to patch");
|
||||
return Ok(HashSet::new());
|
||||
}
|
||||
|
||||
// Preparse all images. Some patchers need to inspect every candidate.
|
||||
let mut images = load_boot_images(names, open_input)?;
|
||||
|
||||
@@ -1147,14 +1263,14 @@ pub fn patch_boot_images<'a>(
|
||||
debug_span!(parent: &parent_span, "patcher", name = p.patcher_name()).entered();
|
||||
p.find_targets(&images, cancel_signal).and_then(|targets| {
|
||||
if targets.is_empty() {
|
||||
Err(Error::NoTargets(p.patcher_name()))
|
||||
Err(TargetsError::NoTargets(p.patcher_name()))
|
||||
} else {
|
||||
debug!("Found patcher targets: {targets:?}");
|
||||
Ok(targets)
|
||||
}
|
||||
})
|
||||
})
|
||||
.collect::<Result<Vec<_>>>()?;
|
||||
.collect::<TargetsResult<Vec<_>>>()?;
|
||||
|
||||
debug!("All patcher targets: {all_targets:?}");
|
||||
|
||||
@@ -1176,54 +1292,22 @@ pub fn patch_boot_images<'a>(
|
||||
// Apply all patches.
|
||||
groups
|
||||
.par_iter_mut()
|
||||
.try_for_each(|(_, (info, patchers))| -> Result<()> {
|
||||
.try_for_each(|(&name, (info, patchers))| -> TargetsResult<()> {
|
||||
patchers.iter().try_for_each(|p| {
|
||||
let _span =
|
||||
debug_span!(parent: &parent_span, "patcher", name = p.patcher_name()).entered();
|
||||
p.patch(&mut info.boot_image, cancel_signal)
|
||||
.map_err(|e| TargetsError::Patch(name.to_owned(), e))
|
||||
})
|
||||
})?;
|
||||
|
||||
// Resign and write new images.
|
||||
groups
|
||||
.par_iter_mut()
|
||||
.map(|(name, (info, _))| {
|
||||
let _span = debug_span!(parent: &parent_span, "image", name).entered();
|
||||
groups.par_iter_mut().try_for_each(|(&name, (info, _))| {
|
||||
let _span = debug_span!(parent: &parent_span, "image", name).entered();
|
||||
let mut writer = open_output(name).map_err(|e| TargetsError::Open(name.to_owned(), e))?;
|
||||
|
||||
let AppendedDescriptorMut::Hash(descriptor) = info.header.appended_descriptor_mut()?
|
||||
else {
|
||||
return Err(Error::NoHashDescriptor);
|
||||
};
|
||||
save_boot_image(&mut writer, info, key).map_err(|e| TargetsError::Save(name.to_owned(), e))
|
||||
})?;
|
||||
|
||||
let writer = open_output(name)?;
|
||||
|
||||
// Write new boot image. We reuse the existing salt for the digest.
|
||||
let mut context = Context::new(&ring::digest::SHA256);
|
||||
context.update(&descriptor.salt);
|
||||
let mut hashing_writer = HashingWriter::new(writer, context);
|
||||
info.boot_image.to_writer(&mut hashing_writer)?;
|
||||
let (mut writer, context) = hashing_writer.finish();
|
||||
|
||||
descriptor.image_size = writer.stream_position()?;
|
||||
"sha256".clone_into(&mut descriptor.hash_algorithm);
|
||||
descriptor.root_digest = context.finish().as_ref().to_vec();
|
||||
|
||||
if !info.header.public_key.is_empty() {
|
||||
debug!("Signing boot image");
|
||||
info.header.set_algo_for_key(key)?;
|
||||
info.header.sign(key)?;
|
||||
}
|
||||
|
||||
avb::write_appended_image(
|
||||
writer,
|
||||
&info.header,
|
||||
&mut info.footer,
|
||||
Some(info.image_size),
|
||||
)?;
|
||||
|
||||
Ok(())
|
||||
})
|
||||
.collect::<Result<()>>()?;
|
||||
|
||||
Ok(groups.keys().cloned().collect())
|
||||
Ok(groups.keys().copied().collect())
|
||||
}
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2023 Andrew Gunnerson
|
||||
* SPDX-License-Identifier: GPL-3.0-only
|
||||
*/
|
||||
// SPDX-FileCopyrightText: 2023 Andrew Gunnerson
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
pub mod boot;
|
||||
pub mod otacert;
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2023 Andrew Gunnerson
|
||||
* SPDX-License-Identifier: GPL-3.0-only
|
||||
*/
|
||||
// SPDX-FileCopyrightText: 2023-2024 Andrew Gunnerson
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use std::{borrow::Cow, cmp::Ordering, io::Cursor};
|
||||
use std::{borrow::Cow, cmp::Ordering, io::Cursor, path::Path};
|
||||
|
||||
use bitflags::bitflags;
|
||||
use thiserror::Error;
|
||||
@@ -19,12 +17,10 @@ pub enum Error {
|
||||
ZipTooSmall(usize),
|
||||
#[error("New otacerts.zip is too large to fit in {0} bytes")]
|
||||
ZipTooLarge(usize),
|
||||
#[error("Crypto error")]
|
||||
Crypto(#[from] crypto::Error),
|
||||
#[error("x509 DER error")]
|
||||
Der(#[from] x509_cert::der::Error),
|
||||
#[error("Zip error")]
|
||||
Zip(#[from] ZipError),
|
||||
#[error("Failed to write otacerts zip")]
|
||||
ZipWrite(#[source] ZipError),
|
||||
#[error("Failed to write certificate to otacerts zip")]
|
||||
CertWrite(#[source] crypto::Error),
|
||||
}
|
||||
|
||||
type Result<T> = std::result::Result<T, Error>;
|
||||
@@ -84,7 +80,8 @@ pub fn create_zip(cert: &Certificate, flags: OtaCertBuildFlags) -> Result<Vec<u8
|
||||
};
|
||||
|
||||
let options = FileOptions::default().compression_method(compression_method);
|
||||
writer.start_file("ota.x509.pem", options)?;
|
||||
let name = "ota.x509.pem";
|
||||
writer.start_file(name, options).map_err(Error::ZipWrite)?;
|
||||
|
||||
let cert = if flags.is_empty() {
|
||||
Cow::Borrowed(cert)
|
||||
@@ -92,7 +89,9 @@ pub fn create_zip(cert: &Certificate, flags: OtaCertBuildFlags) -> Result<Vec<u8
|
||||
let mut modified = cert.clone();
|
||||
|
||||
if flags.contains(OtaCertBuildFlags::REMOVE_SIGNATURE) {
|
||||
modified.signature = BitString::from_bytes(&[])?;
|
||||
// An empty ASN.1 bit string is always valid.
|
||||
modified.signature =
|
||||
BitString::from_bytes(&[]).expect("Empty ASN.1 bit string was invalid");
|
||||
}
|
||||
if flags.contains(OtaCertBuildFlags::REMOVE_EXTENSIONS) {
|
||||
if let Some(extensions) = &mut modified.tbs_certificate.extensions {
|
||||
@@ -111,9 +110,9 @@ pub fn create_zip(cert: &Certificate, flags: OtaCertBuildFlags) -> Result<Vec<u8
|
||||
Cow::Owned(modified)
|
||||
};
|
||||
|
||||
crypto::write_pem_cert(&mut writer, &cert)?;
|
||||
crypto::write_pem_cert(Path::new(name), &mut writer, &cert).map_err(Error::CertWrite)?;
|
||||
|
||||
let raw_writer = writer.finish()?;
|
||||
let raw_writer = writer.finish().map_err(Error::ZipWrite)?;
|
||||
|
||||
Ok(raw_writer.into_inner())
|
||||
}
|
||||
|
||||
+49
-34
@@ -1,7 +1,5 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2023-2024 Andrew Gunnerson
|
||||
* SPDX-License-Identifier: GPL-3.0-only
|
||||
*/
|
||||
// SPDX-FileCopyrightText: 2023-2024 Andrew Gunnerson
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use std::{
|
||||
io::{self, Cursor, SeekFrom},
|
||||
@@ -23,7 +21,7 @@ use crate::{
|
||||
ota,
|
||||
},
|
||||
patch::otacert,
|
||||
stream::{self, ReadSeekReopen, SectionReader, WriteSeekReopen},
|
||||
stream::{self, ReadFixedSizeExt, ReadSeekReopen, SectionReader, WriteSeekReopen},
|
||||
util,
|
||||
};
|
||||
|
||||
@@ -35,14 +33,16 @@ pub enum Error {
|
||||
NoFooter,
|
||||
#[error("No hash tree descriptor found in vbmeta header")]
|
||||
NoHashTreeDescriptor,
|
||||
#[error("{0:?} field is out of bounds")]
|
||||
FieldOutOfBounds(&'static str),
|
||||
#[error("AVB error")]
|
||||
Avb(#[from] avb::Error),
|
||||
#[error("OTA certificate error")]
|
||||
OtaCert(#[from] otacert::Error),
|
||||
#[error("I/O error")]
|
||||
Io(#[from] io::Error),
|
||||
#[error("{0:?} overflowed integer bounds during calculations")]
|
||||
IntOverflow(&'static str),
|
||||
#[error("Failed to update AVB header")]
|
||||
AvbUpdate(#[source] avb::Error),
|
||||
#[error("Failed to generate replacement otacerts zip")]
|
||||
OtaCertZip(#[source] otacert::Error),
|
||||
#[error("Failed to read image data")]
|
||||
ReadData(#[source] io::Error),
|
||||
#[error("Failed to write image data")]
|
||||
WriteData(#[source] io::Error),
|
||||
}
|
||||
|
||||
type Result<T> = std::result::Result<T, Error>;
|
||||
@@ -121,11 +121,15 @@ pub fn patch_system_image(
|
||||
|
||||
let parent_span = Span::current();
|
||||
|
||||
let (mut header, footer, image_size) = avb::load_image(input.reopen_boxed()?)?;
|
||||
let (mut header, footer, image_size) =
|
||||
avb::load_image(input.reopen_boxed().map_err(Error::ReadData)?)
|
||||
.map_err(Error::AvbUpdate)?;
|
||||
let Some(mut footer) = footer else {
|
||||
return Err(Error::NoFooter);
|
||||
};
|
||||
let AppendedDescriptorMut::HashTree(descriptor) = header.appended_descriptor_mut()? else {
|
||||
let AppendedDescriptorMut::HashTree(descriptor) =
|
||||
header.appended_descriptor_mut().map_err(Error::AvbUpdate)?
|
||||
else {
|
||||
return Err(Error::NoHashTreeDescriptor);
|
||||
};
|
||||
|
||||
@@ -135,17 +139,20 @@ pub fn patch_system_image(
|
||||
let modified_ranges = (0..num_chunks)
|
||||
.into_par_iter()
|
||||
.map(|chunk| -> Result<Vec<Range<u64>>> {
|
||||
stream::check_cancel(cancel_signal)?;
|
||||
stream::check_cancel(cancel_signal).map_err(Error::ReadData)?;
|
||||
|
||||
let offset = chunk * CHUNK_SIZE;
|
||||
let size = CHUNK_SIZE.min(footer.original_image_size - offset);
|
||||
let mut buf = vec![0u8; size as usize];
|
||||
|
||||
let mut reader = input.reopen_boxed()?;
|
||||
reader.seek(SeekFrom::Start(offset))?;
|
||||
reader.read_exact(&mut buf)?;
|
||||
let mut reader = input.reopen_boxed().map_err(Error::ReadData)?;
|
||||
reader
|
||||
.seek(SeekFrom::Start(offset))
|
||||
.map_err(Error::ReadData)?;
|
||||
let buf = reader
|
||||
.read_vec_exact(size as usize)
|
||||
.map_err(Error::ReadData)?;
|
||||
|
||||
let mut writer = output.reopen_boxed()?;
|
||||
let mut writer = output.reopen_boxed().map_err(Error::WriteData)?;
|
||||
let mut ranges = Vec::<Range<u64>>::new();
|
||||
|
||||
for eocd_offset_rel in memmem::find_iter(&buf, ota::ZIP_EOCD_MAGIC) {
|
||||
@@ -157,14 +164,17 @@ pub fn patch_system_image(
|
||||
};
|
||||
|
||||
let zip_size = bounds_rel.end - bounds_rel.start;
|
||||
let new_zip = otacert::create_zip_with_size(certificate, zip_size)?;
|
||||
let new_zip = otacert::create_zip_with_size(certificate, zip_size)
|
||||
.map_err(Error::OtaCertZip)?;
|
||||
|
||||
let bounds = offset + bounds_rel.start as u64..offset + bounds_rel.end as u64;
|
||||
|
||||
stream::check_cancel(cancel_signal)?;
|
||||
stream::check_cancel(cancel_signal).map_err(Error::WriteData)?;
|
||||
|
||||
writer.seek(SeekFrom::Start(bounds.start))?;
|
||||
writer.write_all(&new_zip)?;
|
||||
writer
|
||||
.seek(SeekFrom::Start(bounds.start))
|
||||
.map_err(Error::WriteData)?;
|
||||
writer.write_all(&new_zip).map_err(Error::WriteData)?;
|
||||
|
||||
ranges.push(bounds);
|
||||
}
|
||||
@@ -198,18 +208,23 @@ pub fn patch_system_image(
|
||||
Some(modified_ranges.as_slice())
|
||||
};
|
||||
|
||||
descriptor.update(input, output, update_ranges, cancel_signal)?;
|
||||
descriptor
|
||||
.update(input, output, update_ranges, cancel_signal)
|
||||
.map_err(Error::AvbUpdate)?;
|
||||
|
||||
if !header.public_key.is_empty() {
|
||||
debug!("Signing system image");
|
||||
header.set_algo_for_key(key)?;
|
||||
header.sign(key)?;
|
||||
header.set_algo_for_key(key).map_err(Error::AvbUpdate)?;
|
||||
header.sign(key).map_err(Error::AvbUpdate)?;
|
||||
}
|
||||
|
||||
let writer = output.reopen_boxed()?;
|
||||
avb::write_appended_image(writer, &header, &mut footer, Some(image_size))?;
|
||||
let writer = output.reopen_boxed().map_err(Error::WriteData)?;
|
||||
avb::write_appended_image(writer, &header, &mut footer, Some(image_size))
|
||||
.map_err(Error::AvbUpdate)?;
|
||||
|
||||
let AppendedDescriptorMut::HashTree(descriptor) = header.appended_descriptor_mut()? else {
|
||||
let AppendedDescriptorMut::HashTree(descriptor) =
|
||||
header.appended_descriptor_mut().map_err(Error::AvbUpdate)?
|
||||
else {
|
||||
return Err(Error::NoHashTreeDescriptor);
|
||||
};
|
||||
|
||||
@@ -217,15 +232,15 @@ pub fn patch_system_image(
|
||||
let hash_tree_end = descriptor
|
||||
.tree_offset
|
||||
.checked_add(descriptor.tree_size)
|
||||
.ok_or_else(|| Error::FieldOutOfBounds("hash_tree_end"))?;
|
||||
.ok_or(Error::IntOverflow("hash_tree_end"))?;
|
||||
let fec_data_end = descriptor
|
||||
.fec_offset
|
||||
.checked_add(descriptor.fec_size)
|
||||
.ok_or_else(|| Error::FieldOutOfBounds("fec_data_end"))?;
|
||||
.ok_or(Error::IntOverflow("fec_data_end"))?;
|
||||
let header_end = footer
|
||||
.vbmeta_offset
|
||||
.checked_add(footer.vbmeta_size)
|
||||
.ok_or_else(|| Error::FieldOutOfBounds("avb_end"))?;
|
||||
.ok_or(Error::IntOverflow("avb_end"))?;
|
||||
let footer_start = image_size - Footer::SIZE as u64;
|
||||
|
||||
let other_ranges = util::merge_overlapping(&[
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
#![allow(clippy::nursery)]
|
||||
#![allow(clippy::pedantic)]
|
||||
|
||||
pub mod build {
|
||||
pub mod tools {
|
||||
pub mod releasetools {
|
||||
|
||||
+25
-99
@@ -1,7 +1,5 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2023 Andrew Gunnerson
|
||||
* SPDX-License-Identifier: GPL-3.0-only
|
||||
*/
|
||||
// SPDX-FileCopyrightText: 2023 Andrew Gunnerson
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use std::{
|
||||
fs::File,
|
||||
@@ -12,9 +10,8 @@ use std::{
|
||||
},
|
||||
};
|
||||
|
||||
use bstr::ByteSlice;
|
||||
use aws_lc_rs::digest::Context;
|
||||
use num_traits::ToPrimitive;
|
||||
use ring::digest::Context;
|
||||
|
||||
use crate::util;
|
||||
|
||||
@@ -124,69 +121,26 @@ impl<W: Write> WriteZerosExt for W {
|
||||
}
|
||||
}
|
||||
|
||||
/// Extensions for readers to read strings.
|
||||
pub trait ReadStringExt {
|
||||
/// Read exact sized string.
|
||||
fn read_string_exact(&mut self, size: usize) -> io::Result<String>;
|
||||
/// Extensions for readers to read fixed-size buffers.
|
||||
pub trait ReadFixedSizeExt {
|
||||
/// Read fixed-size array.
|
||||
fn read_array_exact<const N: usize>(&mut self) -> io::Result<[u8; N]>;
|
||||
|
||||
/// Read string with maximum size and trim trailing zeros.
|
||||
fn read_string_padded(&mut self, max_size: usize) -> io::Result<String>;
|
||||
/// Read fixed-sized [`Vec`].
|
||||
fn read_vec_exact(&mut self, size: usize) -> io::Result<Vec<u8>>;
|
||||
}
|
||||
|
||||
impl<R: Read> ReadStringExt for R {
|
||||
fn read_string_exact(&mut self, size: usize) -> io::Result<String> {
|
||||
impl<R: Read> ReadFixedSizeExt for R {
|
||||
fn read_array_exact<const N: usize>(&mut self) -> io::Result<[u8; N]> {
|
||||
let mut buf = [0u8; N];
|
||||
self.read_exact(&mut buf)?;
|
||||
Ok(buf)
|
||||
}
|
||||
|
||||
fn read_vec_exact(&mut self, size: usize) -> io::Result<Vec<u8>> {
|
||||
let mut buf = vec![0u8; size];
|
||||
self.read_exact(&mut buf)?;
|
||||
|
||||
String::from_utf8(buf).map_err(|e| {
|
||||
io::Error::new(
|
||||
io::ErrorKind::InvalidData,
|
||||
format!("Invalid UTF-8: {:?}: {e}", e.as_bytes().as_bstr()),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
fn read_string_padded(&mut self, max_size: usize) -> io::Result<String> {
|
||||
let mut buf = vec![0u8; max_size];
|
||||
self.read_exact(&mut buf)?;
|
||||
|
||||
let after_last_non_zero = buf
|
||||
.iter()
|
||||
.rev()
|
||||
.position(|&b| b != 0)
|
||||
.map_or(0, |i| buf.len() - i);
|
||||
buf.resize(after_last_non_zero, 0);
|
||||
buf.shrink_to_fit();
|
||||
|
||||
String::from_utf8(buf).map_err(|e| {
|
||||
io::Error::new(
|
||||
io::ErrorKind::InvalidData,
|
||||
format!("Invalid UTF-8: {:?}: {e}", e.as_bytes().as_bstr()),
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Extensions for writers to write strings.
|
||||
pub trait WriteStringExt {
|
||||
fn write_string_padded(&mut self, data: &str, max_size: usize) -> io::Result<()>;
|
||||
}
|
||||
|
||||
impl<W: Write> WriteStringExt for W {
|
||||
fn write_string_padded(&mut self, data: &str, max_size: usize) -> io::Result<()> {
|
||||
if data.len() > max_size {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidInput,
|
||||
format!("{data:?} exceeds maximum size of {max_size} bytes"),
|
||||
));
|
||||
}
|
||||
|
||||
self.write_all(data.as_bytes())?;
|
||||
|
||||
let num_zeros = (max_size - data.len()) as u64;
|
||||
self.write_zeros_exact(num_zeros)?;
|
||||
|
||||
Ok(())
|
||||
Ok(buf)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -199,13 +153,13 @@ pub trait Reopen: Sized {
|
||||
|
||||
impl<R: Read + Reopen> Reopen for BufReader<R> {
|
||||
fn reopen(&self) -> io::Result<Self> {
|
||||
Ok(BufReader::new(self.get_ref().reopen()?))
|
||||
Ok(Self::new(self.get_ref().reopen()?))
|
||||
}
|
||||
}
|
||||
|
||||
impl<W: Write + Reopen> Reopen for BufWriter<W> {
|
||||
fn reopen(&self) -> io::Result<Self> {
|
||||
Ok(BufWriter::new(self.get_ref().reopen()?))
|
||||
Ok(Self::new(self.get_ref().reopen()?))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -545,9 +499,7 @@ pub struct SharedCursor {
|
||||
|
||||
impl SharedCursor {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
..Default::default()
|
||||
}
|
||||
Self::default()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -685,11 +637,11 @@ mod tests {
|
||||
sync::atomic::{AtomicBool, Ordering},
|
||||
};
|
||||
|
||||
use ring::digest::Context;
|
||||
use aws_lc_rs::digest::Context;
|
||||
|
||||
use super::{
|
||||
CountingReader, CountingWriter, HashingReader, HashingWriter, PSeekFile, ReadDiscardExt,
|
||||
ReadStringExt, Reopen, SectionReader, SharedCursor, WriteStringExt, WriteZerosExt,
|
||||
Reopen, SectionReader, SharedCursor, WriteZerosExt,
|
||||
};
|
||||
|
||||
const FOOBAR_SHA256: [u8; 32] = [
|
||||
@@ -726,32 +678,6 @@ mod tests {
|
||||
assert_eq!(&writer.into_inner(), b"\0\0foo\0");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn read_string() {
|
||||
let mut reader = Cursor::new(b"foo\0\0bar\0\0");
|
||||
|
||||
assert_eq!(reader.read_string_exact(3).unwrap(), "foo");
|
||||
assert_eq!(reader.read_string_exact(0).unwrap(), "");
|
||||
|
||||
reader.rewind().unwrap();
|
||||
assert_eq!(reader.read_string_padded(3).unwrap(), "foo");
|
||||
|
||||
reader.rewind().unwrap();
|
||||
assert_eq!(reader.read_string_padded(10).unwrap(), "foo\0\0bar");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn write_string() {
|
||||
let mut writer = Cursor::new([0xffu8; 8]);
|
||||
|
||||
writer.write_string_padded("foobar", 8).unwrap();
|
||||
assert_eq!(writer.get_ref(), b"foobar\0\0");
|
||||
|
||||
writer.rewind().unwrap();
|
||||
writer.write_string_padded("foobarhi", 8).unwrap();
|
||||
assert_eq!(writer.get_ref(), b"foobarhi");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn counting_reader() {
|
||||
let raw_reader = Cursor::new(b"foobar");
|
||||
@@ -787,7 +713,7 @@ mod tests {
|
||||
#[test]
|
||||
fn hashing_reader() {
|
||||
let raw_reader = Cursor::new(b"foobar");
|
||||
let mut reader = HashingReader::new(raw_reader, Context::new(&ring::digest::SHA256));
|
||||
let mut reader = HashingReader::new(raw_reader, Context::new(&aws_lc_rs::digest::SHA256));
|
||||
|
||||
let mut buf = [0u8; 6];
|
||||
reader.read_exact(&mut buf[..0]).unwrap();
|
||||
@@ -804,7 +730,7 @@ mod tests {
|
||||
#[test]
|
||||
fn hashing_writer() {
|
||||
let raw_writer = Cursor::new([0u8; 6]);
|
||||
let mut writer = HashingWriter::new(raw_writer, Context::new(&ring::digest::SHA256));
|
||||
let mut writer = HashingWriter::new(raw_writer, Context::new(&aws_lc_rs::digest::SHA256));
|
||||
|
||||
writer.write_all(b"").unwrap();
|
||||
writer.write_all(b"foo").unwrap();
|
||||
|
||||
+331
-20
@@ -1,11 +1,17 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2023 Andrew Gunnerson
|
||||
* SPDX-License-Identifier: GPL-3.0-only
|
||||
*/
|
||||
// SPDX-FileCopyrightText: 2023-2024 Andrew Gunnerson
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use std::{cmp::Ordering, fmt, ops::Range, path::Path};
|
||||
use std::{
|
||||
cmp::Ordering,
|
||||
fmt, mem,
|
||||
ops::{
|
||||
Bound, Range, RangeBounds, RangeFrom, RangeFull, RangeInclusive, RangeTo, RangeToInclusive,
|
||||
},
|
||||
path::Path,
|
||||
};
|
||||
|
||||
use num_traits::PrimInt;
|
||||
use num_traits::{NumCast, PrimInt};
|
||||
use thiserror::Error;
|
||||
|
||||
pub const ZEROS: [u8; 16384] = [0u8; 16384];
|
||||
|
||||
@@ -23,6 +29,263 @@ impl<T: PrimInt + fmt::Debug> fmt::Debug for NumBytes<T> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Stores a precomputed [`Debug`] string.
|
||||
#[derive(Clone)]
|
||||
pub struct DebugString(String);
|
||||
|
||||
impl DebugString {
|
||||
pub fn new(value: impl fmt::Debug) -> Self {
|
||||
Self(format!("{value:?}"))
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Debug for DebugString {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.write_str(&self.0)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Hash, PartialEq, Eq)]
|
||||
pub enum AnyRange<T> {
|
||||
Range(Range<T>),
|
||||
RangeFrom(RangeFrom<T>),
|
||||
RangeFull(RangeFull),
|
||||
RangeInclusive(RangeInclusive<T>),
|
||||
RangeTo(RangeTo<T>),
|
||||
RangeToInclusive(RangeToInclusive<T>),
|
||||
}
|
||||
|
||||
impl<T> AnyRange<T> {
|
||||
pub fn with_bounds(start: Bound<T>, end: Bound<T>) -> Option<Self> {
|
||||
let result = match (start, end) {
|
||||
(Bound::Included(s), Bound::Excluded(e)) => Self::Range(s..e),
|
||||
(Bound::Included(s), Bound::Unbounded) => Self::RangeFrom(s..),
|
||||
(Bound::Unbounded, Bound::Unbounded) => Self::RangeFull(..),
|
||||
(Bound::Included(s), Bound::Included(e)) => Self::RangeInclusive(s..=e),
|
||||
(Bound::Unbounded, Bound::Excluded(e)) => Self::RangeTo(..e),
|
||||
(Bound::Unbounded, Bound::Included(e)) => Self::RangeToInclusive(..=e),
|
||||
(Bound::Excluded(_), _) => return None,
|
||||
};
|
||||
|
||||
Some(result)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: PartialOrd<T>> AnyRange<T> {
|
||||
pub fn contains<U>(&self, item: &U) -> bool
|
||||
where
|
||||
T: PartialOrd<U>,
|
||||
U: ?Sized + PartialOrd<T>,
|
||||
{
|
||||
<Self as RangeBounds<T>>::contains(self, item)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: fmt::Debug> fmt::Debug for AnyRange<T> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Self::Range(r) => r.fmt(f),
|
||||
Self::RangeFrom(r) => r.fmt(f),
|
||||
Self::RangeFull(r) => r.fmt(f),
|
||||
Self::RangeInclusive(r) => r.fmt(f),
|
||||
Self::RangeTo(r) => r.fmt(f),
|
||||
Self::RangeToInclusive(r) => r.fmt(f),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> RangeBounds<T> for AnyRange<T> {
|
||||
fn start_bound(&self) -> Bound<&T> {
|
||||
match self {
|
||||
Self::Range(r) => r.start_bound(),
|
||||
Self::RangeFrom(r) => r.start_bound(),
|
||||
Self::RangeFull(r) => r.start_bound(),
|
||||
Self::RangeInclusive(r) => r.start_bound(),
|
||||
Self::RangeTo(r) => r.start_bound(),
|
||||
Self::RangeToInclusive(r) => r.start_bound(),
|
||||
}
|
||||
}
|
||||
|
||||
fn end_bound(&self) -> Bound<&T> {
|
||||
match self {
|
||||
Self::Range(r) => r.end_bound(),
|
||||
Self::RangeFrom(r) => r.end_bound(),
|
||||
Self::RangeFull(r) => r.end_bound(),
|
||||
Self::RangeInclusive(r) => r.end_bound(),
|
||||
Self::RangeTo(r) => r.end_bound(),
|
||||
Self::RangeToInclusive(r) => r.end_bound(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> From<Range<T>> for AnyRange<T> {
|
||||
fn from(value: Range<T>) -> Self {
|
||||
Self::Range(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> From<RangeFrom<T>> for AnyRange<T> {
|
||||
fn from(value: RangeFrom<T>) -> Self {
|
||||
Self::RangeFrom(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> From<RangeFull> for AnyRange<T> {
|
||||
fn from(value: RangeFull) -> Self {
|
||||
Self::RangeFull(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> From<RangeInclusive<T>> for AnyRange<T> {
|
||||
fn from(value: RangeInclusive<T>) -> Self {
|
||||
Self::RangeInclusive(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> From<RangeTo<T>> for AnyRange<T> {
|
||||
fn from(value: RangeTo<T>) -> Self {
|
||||
Self::RangeTo(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> From<RangeToInclusive<T>> for AnyRange<T> {
|
||||
fn from(value: RangeToInclusive<T>) -> Self {
|
||||
Self::RangeToInclusive(value)
|
||||
}
|
||||
}
|
||||
|
||||
/// A non-generic type that can represent any 64-bit or smaller primitive
|
||||
/// integer.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum LargeInt {
|
||||
Signed(i64),
|
||||
Unsigned(u64),
|
||||
}
|
||||
|
||||
impl fmt::Display for LargeInt {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Self::Signed(n) => n.fmt(f),
|
||||
Self::Unsigned(n) => n.fmt(f),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A non-generic type that can represent any 64-bit or smaller primitive
|
||||
/// integer range.
|
||||
#[derive(Clone, PartialEq, Eq)]
|
||||
pub enum LargeIntRange {
|
||||
Signed(AnyRange<i64>),
|
||||
Unsigned(AnyRange<u64>),
|
||||
}
|
||||
|
||||
impl fmt::Debug for LargeIntRange {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Self::Signed(r) => r.fmt(f),
|
||||
Self::Unsigned(r) => r.fmt(f),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// An error returned when a value is not within a specific range.
|
||||
#[derive(Clone, Debug, Error)]
|
||||
#[error("Integer value {value} not in bounds: {range:?}")]
|
||||
pub struct OutOfBoundsError {
|
||||
value: LargeInt,
|
||||
range: LargeIntRange,
|
||||
}
|
||||
|
||||
/// Verify that `value` is within `bounds` and then return `value` if it is.
|
||||
pub fn check_bounds<T: PrimInt>(
|
||||
value: T,
|
||||
range: impl Into<AnyRange<T>>,
|
||||
) -> Result<T, OutOfBoundsError> {
|
||||
const {
|
||||
assert!(
|
||||
mem::size_of::<T>() <= 8,
|
||||
"Integer must be 64 bits or smaller"
|
||||
);
|
||||
}
|
||||
|
||||
let range = range.into();
|
||||
|
||||
if !range.contains(&value) {
|
||||
let value = if T::min_value() != T::zero() {
|
||||
LargeInt::Signed(NumCast::from(value).unwrap())
|
||||
} else {
|
||||
LargeInt::Unsigned(NumCast::from(value).unwrap())
|
||||
};
|
||||
|
||||
let range = if T::min_value() != T::zero() {
|
||||
let start = match range.start_bound() {
|
||||
Bound::Excluded(n) => Bound::Excluded(NumCast::from(*n).unwrap()),
|
||||
Bound::Included(n) => Bound::Included(NumCast::from(*n).unwrap()),
|
||||
Bound::Unbounded => Bound::Unbounded,
|
||||
};
|
||||
|
||||
let end = match range.end_bound() {
|
||||
Bound::Excluded(n) => Bound::Excluded(NumCast::from(*n).unwrap()),
|
||||
Bound::Included(n) => Bound::Included(NumCast::from(*n).unwrap()),
|
||||
Bound::Unbounded => Bound::Unbounded,
|
||||
};
|
||||
|
||||
LargeIntRange::Signed(AnyRange::with_bounds(start, end).unwrap())
|
||||
} else {
|
||||
let start = match range.start_bound() {
|
||||
Bound::Excluded(n) => Bound::Excluded(NumCast::from(*n).unwrap()),
|
||||
Bound::Included(n) => Bound::Included(NumCast::from(*n).unwrap()),
|
||||
Bound::Unbounded => Bound::Unbounded,
|
||||
};
|
||||
|
||||
let end = match range.end_bound() {
|
||||
Bound::Excluded(n) => Bound::Excluded(NumCast::from(*n).unwrap()),
|
||||
Bound::Included(n) => Bound::Included(NumCast::from(*n).unwrap()),
|
||||
Bound::Unbounded => Bound::Unbounded,
|
||||
};
|
||||
|
||||
LargeIntRange::Unsigned(AnyRange::with_bounds(start, end).unwrap())
|
||||
};
|
||||
|
||||
return Err(OutOfBoundsError { value, range });
|
||||
}
|
||||
|
||||
Ok(value)
|
||||
}
|
||||
|
||||
/// Try to cast `value` to primitive integer type `T`. If it does not fit, the
|
||||
/// error will indicate the valid range of values.
|
||||
pub fn try_cast<T: PrimInt, V: PrimInt>(value: V) -> Result<T, OutOfBoundsError> {
|
||||
const {
|
||||
assert!(
|
||||
mem::size_of::<T>() <= 8,
|
||||
"Integer must be 64 bits or smaller"
|
||||
);
|
||||
}
|
||||
|
||||
NumCast::from(value).ok_or_else(|| {
|
||||
let value = if V::min_value() != V::zero() {
|
||||
LargeInt::Signed(NumCast::from(value).unwrap())
|
||||
} else {
|
||||
LargeInt::Unsigned(NumCast::from(value).unwrap())
|
||||
};
|
||||
|
||||
let range = if T::min_value() != T::zero() {
|
||||
let min = NumCast::from(T::min_value()).unwrap();
|
||||
let max = NumCast::from(T::max_value()).unwrap();
|
||||
|
||||
LargeIntRange::Signed((min..=max).into())
|
||||
} else {
|
||||
let min = NumCast::from(T::min_value()).unwrap();
|
||||
let max = NumCast::from(T::max_value()).unwrap();
|
||||
|
||||
LargeIntRange::Unsigned((min..=max).into())
|
||||
};
|
||||
|
||||
OutOfBoundsError { value, range }
|
||||
})
|
||||
}
|
||||
|
||||
/// Check if a byte slice is all zeros.
|
||||
pub fn is_zero(mut buf: &[u8]) -> bool {
|
||||
while !buf.is_empty() {
|
||||
@@ -119,25 +382,73 @@ where
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_any_range() {
|
||||
let range = AnyRange::with_bounds(Bound::Included(0), Bound::Excluded(1)).unwrap();
|
||||
assert_eq!(range, AnyRange::from(0..1));
|
||||
|
||||
let range = AnyRange::with_bounds(Bound::Included(0), Bound::Unbounded).unwrap();
|
||||
assert_eq!(range, AnyRange::from(0..));
|
||||
|
||||
let range = AnyRange::<i32>::with_bounds(Bound::Unbounded, Bound::Unbounded).unwrap();
|
||||
assert_eq!(range, AnyRange::from(..));
|
||||
|
||||
let range = AnyRange::with_bounds(Bound::Included(0), Bound::Included(1)).unwrap();
|
||||
assert_eq!(range, AnyRange::from(0..=1));
|
||||
|
||||
let range = AnyRange::with_bounds(Bound::Unbounded, Bound::Excluded(1)).unwrap();
|
||||
assert_eq!(range, AnyRange::from(..1));
|
||||
|
||||
let range = AnyRange::with_bounds(Bound::Unbounded, Bound::Included(1)).unwrap();
|
||||
assert_eq!(range, AnyRange::from(..=1));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_check_bounds() {
|
||||
check_bounds(i64::MIN, ..).unwrap();
|
||||
check_bounds(i64::MAX, ..).unwrap();
|
||||
check_bounds(u64::MIN, ..).unwrap();
|
||||
check_bounds(u64::MAX, ..).unwrap();
|
||||
check_bounds(0, -1..=1).unwrap();
|
||||
|
||||
let err = check_bounds(i8::MAX, 0..=0).unwrap_err();
|
||||
assert_eq!(err.value, LargeInt::Signed(127));
|
||||
assert_eq!(err.range, LargeIntRange::Signed(AnyRange::from(0..=0)));
|
||||
|
||||
let err = check_bounds(u8::MAX, 0..=0).unwrap_err();
|
||||
assert_eq!(err.value, LargeInt::Unsigned(255));
|
||||
assert_eq!(err.range, LargeIntRange::Unsigned(AnyRange::from(0..=0)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_try_cast() {
|
||||
let value: u8 = try_cast(255u16).unwrap();
|
||||
assert_eq!(value, 255);
|
||||
|
||||
let err = try_cast::<i8, _>(256u16).unwrap_err();
|
||||
assert_eq!(err.value, LargeInt::Unsigned(256));
|
||||
assert_eq!(err.range, LargeIntRange::Signed(AnyRange::from(-128..=127)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_ranges_overlaps() {
|
||||
assert_eq!(ranges_overlaps(&[0..4], &(0..0)), false);
|
||||
assert_eq!(ranges_overlaps(&[0..4], &(0..4)), true);
|
||||
assert_eq!(ranges_overlaps(&[0..4], &(1..4)), true);
|
||||
assert_eq!(ranges_overlaps(&[0..4], &(0..3)), true);
|
||||
assert_eq!(ranges_overlaps(&[0..4], &(4..5)), false);
|
||||
assert_eq!(ranges_overlaps(&[5..8], &(5..9)), true);
|
||||
assert_eq!(ranges_overlaps(&[5..8], &(4..8)), true);
|
||||
assert_eq!(ranges_overlaps(&[5..8], &(4..9)), true);
|
||||
assert_eq!(ranges_overlaps(&[0..4, 5..8], &(4..5)), true);
|
||||
assert_eq!(ranges_overlaps(&[0..4, 5..8], &(0..9)), true);
|
||||
assert!(!ranges_overlaps(&[0..4], &(0..0)));
|
||||
assert!(ranges_overlaps(&[0..4], &(0..4)));
|
||||
assert!(ranges_overlaps(&[0..4], &(1..4)));
|
||||
assert!(ranges_overlaps(&[0..4], &(0..3)));
|
||||
assert!(!ranges_overlaps(&[0..4], &(4..5)));
|
||||
assert!(ranges_overlaps(&[5..8], &(5..9)));
|
||||
assert!(ranges_overlaps(&[5..8], &(4..8)));
|
||||
assert!(ranges_overlaps(&[5..8], &(4..9)));
|
||||
assert!(ranges_overlaps(&[0..4, 5..8], &(4..5)));
|
||||
assert!(ranges_overlaps(&[0..4, 5..8], &(0..9)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_ranges_contains() {
|
||||
assert_eq!(ranges_contains(&[0..4], &0), true);
|
||||
assert_eq!(ranges_contains(&[0..4], &4), false);
|
||||
assert_eq!(ranges_contains(&[0..4, 5..8], &4), false);
|
||||
assert_eq!(ranges_contains(&[0..4, 5..8], &6), true);
|
||||
assert!(ranges_contains(&[0..4], &0));
|
||||
assert!(!ranges_contains(&[0..4], &4));
|
||||
assert!(!ranges_contains(&[0..4, 5..8], &4));
|
||||
assert!(ranges_contains(&[0..4, 5..8], &6));
|
||||
}
|
||||
}
|
||||
|
||||
+30
-32
@@ -1,7 +1,5 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2023-2024 Andrew Gunnerson
|
||||
* SPDX-License-Identifier: GPL-3.0-only
|
||||
*/
|
||||
// SPDX-FileCopyrightText: 2023-2024 Andrew Gunnerson
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use std::{
|
||||
io::{Cursor, Read, Seek, Write},
|
||||
@@ -120,7 +118,7 @@ fn round_trip_root_image() {
|
||||
rollback_index: 1677974400,
|
||||
flags: 0,
|
||||
rollback_index_location: 0,
|
||||
release_string: repeat_str("MaxLength", 48),
|
||||
release_string: repeat_str("MaxLength", 47),
|
||||
reserved: repeat_array(&[0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef]),
|
||||
};
|
||||
|
||||
@@ -136,13 +134,13 @@ fn round_trip_root_image() {
|
||||
|
||||
// Verify checksum of the output.
|
||||
assert_eq!(
|
||||
ring::digest::digest(&ring::digest::SHA512, &data).as_ref(),
|
||||
aws_lc_rs::digest::digest(&aws_lc_rs::digest::SHA512, &data).as_ref(),
|
||||
[
|
||||
0xc4, 0xa5, 0xda, 0x3e, 0x09, 0xa2, 0xc8, 0x70, 0xcb, 0xf0, 0x96, 0x79, 0x0e, 0x1e,
|
||||
0x80, 0xae, 0x5e, 0x37, 0x81, 0x27, 0x24, 0xc3, 0x6c, 0xa9, 0x42, 0x9e, 0x2c, 0xb1,
|
||||
0x81, 0xad, 0xce, 0xee, 0x8d, 0x4f, 0x76, 0x45, 0x54, 0xc1, 0x31, 0x6a, 0xa7, 0x81,
|
||||
0x5c, 0x59, 0xa8, 0xe8, 0x76, 0xab, 0xed, 0x5b, 0x07, 0x07, 0x38, 0xdd, 0x09, 0x86,
|
||||
0x05, 0x39, 0x23, 0x2d, 0x7b, 0xcc, 0x57, 0x06,
|
||||
0x3b, 0x01, 0xf6, 0x04, 0x04, 0x6e, 0x6f, 0x60, 0x9c, 0xb0, 0x8b, 0x8a, 0x43, 0xf7,
|
||||
0x91, 0x2e, 0xc4, 0x1b, 0xc0, 0x7f, 0xa1, 0xe4, 0xe6, 0x59, 0x14, 0x08, 0xbe, 0x83,
|
||||
0xae, 0x0a, 0x0f, 0x0a, 0x4a, 0x15, 0x91, 0x0e, 0x4d, 0x18, 0x31, 0x48, 0x20, 0xe8,
|
||||
0x44, 0x62, 0x07, 0x98, 0x43, 0x30, 0xee, 0x2d, 0x20, 0x28, 0xc3, 0x94, 0xc6, 0x0e,
|
||||
0x86, 0xa3, 0xa7, 0x17, 0x36, 0xfd, 0x50, 0x7c,
|
||||
],
|
||||
);
|
||||
|
||||
@@ -188,7 +186,7 @@ fn round_trip_appended_hash_image() {
|
||||
rollback_index: 1677974400,
|
||||
flags: 0,
|
||||
rollback_index_location: 0,
|
||||
release_string: repeat_str("MaxLength", 48),
|
||||
release_string: repeat_str("MaxLength", 47),
|
||||
reserved: repeat_array(&[0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef]),
|
||||
};
|
||||
let mut footer = Footer {
|
||||
@@ -235,13 +233,13 @@ fn round_trip_appended_hash_image() {
|
||||
|
||||
// Verify checksum of the output.
|
||||
assert_eq!(
|
||||
ring::digest::digest(&ring::digest::SHA512, &data).as_ref(),
|
||||
aws_lc_rs::digest::digest(&aws_lc_rs::digest::SHA512, &data).as_ref(),
|
||||
[
|
||||
0x09, 0x98, 0x0c, 0x9d, 0x11, 0x50, 0xde, 0xb1, 0x55, 0x3b, 0x00, 0x76, 0xbe, 0x25,
|
||||
0xfd, 0xe6, 0x46, 0x22, 0xbd, 0x9a, 0x05, 0x86, 0xea, 0x07, 0x4d, 0x8f, 0x7b, 0x15,
|
||||
0x36, 0x20, 0x0d, 0xf0, 0x7e, 0x96, 0xd2, 0x58, 0xde, 0xf2, 0xa6, 0x91, 0x6d, 0x01,
|
||||
0x7b, 0x03, 0x96, 0x70, 0xf8, 0x3b, 0x76, 0x74, 0xf0, 0xbf, 0x47, 0xe0, 0xd2, 0xd4,
|
||||
0x5d, 0xbf, 0xb7, 0x9c, 0xf5, 0xf8, 0xaf, 0x3c,
|
||||
0x91, 0x38, 0x61, 0xc0, 0x68, 0x2a, 0x8b, 0xd8, 0x01, 0xa6, 0xe4, 0x4c, 0x1d, 0x27,
|
||||
0x93, 0x1b, 0xa4, 0x63, 0xd1, 0xbb, 0xf1, 0x64, 0x05, 0xf2, 0xa1, 0xa0, 0xb3, 0x35,
|
||||
0xe1, 0xc5, 0xac, 0x4f, 0x98, 0xb3, 0x0a, 0xed, 0xfc, 0xee, 0xa2, 0x6a, 0x77, 0xf4,
|
||||
0xe5, 0x69, 0xa0, 0xcd, 0x7a, 0xd1, 0xfe, 0x1d, 0x07, 0xd1, 0x25, 0xc6, 0x22, 0xe0,
|
||||
0x25, 0xcb, 0xe9, 0x75, 0x50, 0xe4, 0xae, 0x59,
|
||||
],
|
||||
);
|
||||
|
||||
@@ -296,7 +294,7 @@ fn round_trip_appended_hash_tree_image_fixed_size() {
|
||||
rollback_index: 1677974400,
|
||||
flags: 0,
|
||||
rollback_index_location: 0,
|
||||
release_string: repeat_str("MaxLength", 48),
|
||||
release_string: repeat_str("MaxLength", 47),
|
||||
reserved: repeat_array(&[0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef]),
|
||||
};
|
||||
let mut footer = Footer {
|
||||
@@ -343,13 +341,13 @@ fn round_trip_appended_hash_tree_image_fixed_size() {
|
||||
|
||||
// Verify checksum of the output.
|
||||
assert_eq!(
|
||||
ring::digest::digest(&ring::digest::SHA512, &data).as_ref(),
|
||||
aws_lc_rs::digest::digest(&aws_lc_rs::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
|
||||
0x92, 0xdd, 0x4d, 0xc5, 0xb0, 0x5b, 0x4f, 0x65, 0x97, 0x5a, 0x72, 0x66, 0xde, 0x82,
|
||||
0xc2, 0x2f, 0x33, 0x86, 0x8b, 0x65, 0x67, 0x80, 0x1d, 0xca, 0xd6, 0x2c, 0xfc, 0xca,
|
||||
0xaf, 0x4c, 0x56, 0x64, 0x3a, 0xd1, 0x06, 0x01, 0xda, 0x2e, 0x05, 0x67, 0xd1, 0x01,
|
||||
0xe3, 0xcb, 0x7b, 0x1e, 0xeb, 0x05, 0x89, 0xeb, 0x80, 0xcc, 0x17, 0x0c, 0x24, 0x73,
|
||||
0x0d, 0xcb, 0x36, 0xfa, 0x17, 0xbd, 0x20, 0x7e,
|
||||
],
|
||||
);
|
||||
|
||||
@@ -403,7 +401,7 @@ fn round_trip_appended_hash_tree_image_minimum_size() {
|
||||
rollback_index: 1677974400,
|
||||
flags: 0,
|
||||
rollback_index_location: 0,
|
||||
release_string: repeat_str("MaxLength", 48),
|
||||
release_string: repeat_str("MaxLength", 47),
|
||||
reserved: repeat_array(&[0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef]),
|
||||
};
|
||||
let mut footer = Footer {
|
||||
@@ -450,13 +448,13 @@ fn round_trip_appended_hash_tree_image_minimum_size() {
|
||||
|
||||
// Verify checksum of the output.
|
||||
assert_eq!(
|
||||
ring::digest::digest(&ring::digest::SHA512, &data).as_ref(),
|
||||
aws_lc_rs::digest::digest(&aws_lc_rs::digest::SHA512, &data).as_ref(),
|
||||
[
|
||||
0xd6, 0x69, 0x19, 0x6a, 0x36, 0xc8, 0x1c, 0xe9, 0xc4, 0x85, 0xbe, 0xff, 0x43, 0xb1,
|
||||
0x9f, 0xd4, 0x1d, 0x6c, 0xf9, 0xd2, 0xf3, 0xa6, 0x5f, 0x66, 0x41, 0xd5, 0xf3, 0xfd,
|
||||
0x28, 0xdb, 0x14, 0x67, 0xc6, 0xa8, 0xef, 0xc4, 0xd4, 0x67, 0x6c, 0xb8, 0x66, 0xbb,
|
||||
0x56, 0x5a, 0x4a, 0xf5, 0xd8, 0x92, 0x7c, 0x42, 0xbc, 0x47, 0xdb, 0x94, 0x38, 0x15,
|
||||
0x4b, 0x2d, 0xd0, 0x28, 0x1f, 0xd1, 0x45, 0xa9,
|
||||
0xcf, 0x6b, 0x90, 0xcf, 0x77, 0x76, 0x62, 0x12, 0xc2, 0x22, 0xe6, 0xd5, 0x5b, 0xab,
|
||||
0x82, 0xd8, 0x6c, 0x93, 0xa3, 0x35, 0x5b, 0x77, 0xe0, 0x38, 0x12, 0x48, 0x90, 0x0c,
|
||||
0xee, 0xbf, 0x95, 0x31, 0xff, 0xc7, 0xf5, 0xb9, 0x4f, 0x18, 0x57, 0x46, 0x37, 0xbb,
|
||||
0xce, 0x7b, 0xa7, 0x26, 0x18, 0x5a, 0x3c, 0x41, 0xb2, 0x2e, 0xb7, 0x86, 0x51, 0xdc,
|
||||
0xf6, 0x26, 0x86, 0xf3, 0xc7, 0x96, 0x23, 0xed,
|
||||
],
|
||||
);
|
||||
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2023-2024 Andrew Gunnerson
|
||||
* SPDX-License-Identifier: GPL-3.0-only
|
||||
*/
|
||||
// SPDX-FileCopyrightText: 2023-2024 Andrew Gunnerson
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use std::io::Cursor;
|
||||
|
||||
@@ -51,7 +49,7 @@ fn round_trip(image: &BootImage, sha512: &[u8; 64], expected_version: u32) {
|
||||
let data = writer.into_inner();
|
||||
|
||||
assert_eq!(
|
||||
ring::digest::digest(&ring::digest::SHA512, &data).as_ref(),
|
||||
aws_lc_rs::digest::digest(&aws_lc_rs::digest::SHA512, &data).as_ref(),
|
||||
sha512,
|
||||
);
|
||||
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2023 Andrew Gunnerson
|
||||
* SPDX-License-Identifier: GPL-3.0-only
|
||||
*/
|
||||
// SPDX-FileCopyrightText: 2023 Andrew Gunnerson
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use std::io::{Cursor, Read, Seek, Write};
|
||||
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2023-2024 Andrew Gunnerson
|
||||
* SPDX-License-Identifier: GPL-3.0-only
|
||||
*/
|
||||
// SPDX-FileCopyrightText: 2023-2024 Andrew Gunnerson
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use std::io::{self, Cursor};
|
||||
|
||||
@@ -43,7 +41,7 @@ fn generate_archive() -> Vec<u8> {
|
||||
let data = writer.into_inner();
|
||||
|
||||
assert_eq!(
|
||||
ring::digest::digest(&ring::digest::SHA512, &data).as_ref(),
|
||||
aws_lc_rs::digest::digest(&aws_lc_rs::digest::SHA512, &data).as_ref(),
|
||||
[
|
||||
0xb0, 0x51, 0xac, 0x28, 0x6f, 0x78, 0xe2, 0xe7, 0x45, 0xa0, 0x52, 0x7c, 0xff, 0x42,
|
||||
0x30, 0x55, 0xbd, 0x64, 0x7d, 0x4e, 0xb8, 0xe6, 0x95, 0xe5, 0x9b, 0xd1, 0x13, 0xd6,
|
||||
|
||||
+3
-5
@@ -1,7 +1,5 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2024 Andrew Gunnerson
|
||||
* SPDX-License-Identifier: GPL-3.0-only
|
||||
*/
|
||||
// SPDX-FileCopyrightText: 2024 Andrew Gunnerson
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use std::{io::Cursor, num::NonZeroU64};
|
||||
|
||||
@@ -19,7 +17,7 @@ fn round_trip(metadata: &Metadata, sha512: &[u8; 64]) {
|
||||
let data = writer.into_inner();
|
||||
|
||||
assert_eq!(
|
||||
ring::digest::digest(&ring::digest::SHA512, &data).as_ref(),
|
||||
aws_lc_rs::digest::digest(&aws_lc_rs::digest::SHA512, &data).as_ref(),
|
||||
sha512,
|
||||
);
|
||||
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2024 Andrew Gunnerson
|
||||
* SPDX-License-Identifier: GPL-3.0-only
|
||||
*/
|
||||
// SPDX-FileCopyrightText: 2024 Andrew Gunnerson
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use std::io::{Cursor, Read, Write};
|
||||
|
||||
@@ -41,7 +39,7 @@ fn round_trip(block_size: u32, crc32: u32, test_chunks: &[TestChunk], sha512: &[
|
||||
let data = writer.into_inner();
|
||||
|
||||
assert_eq!(
|
||||
ring::digest::digest(&ring::digest::SHA512, &data).as_ref(),
|
||||
aws_lc_rs::digest::digest(&aws_lc_rs::digest::SHA512, &data).as_ref(),
|
||||
sha512,
|
||||
);
|
||||
|
||||
|
||||
@@ -33,19 +33,13 @@ allow = [
|
||||
"Apache-2.0",
|
||||
"Apache-2.0 WITH LLVM-exception",
|
||||
"BSD-3-Clause",
|
||||
"CC0-1.0",
|
||||
"bzip2-1.0.6",
|
||||
"GPL-3.0",
|
||||
"ISC",
|
||||
"MIT",
|
||||
"OpenSSL",
|
||||
"Unicode-DFS-2016",
|
||||
]
|
||||
|
||||
[[licenses.clarify]]
|
||||
name = "ring"
|
||||
expression = "MIT AND ISC AND OpenSSL"
|
||||
license-files = [
|
||||
{ path = "LICENSE", hash = 0xbd0eed23 },
|
||||
"Unicode-3.0",
|
||||
"Zlib",
|
||||
]
|
||||
|
||||
[bans]
|
||||
@@ -63,12 +57,13 @@ include-workspace = true
|
||||
bypass = [
|
||||
# Copies of unmodified crashwrangler objects for old macOS versions.
|
||||
{ name = "honggfuzz", allow-globs = ["honggfuzz/third_party/mac/CrashReport_*.o"] },
|
||||
# Only used in tests.
|
||||
{ name = "libloading", allow-globs = ["tests/*.dll"] }
|
||||
]
|
||||
|
||||
[sources]
|
||||
unknown-registry = "deny"
|
||||
unknown-git = "deny"
|
||||
allow-git = [
|
||||
"https://github.com/chenxiaolong/bzip2-rs",
|
||||
"https://github.com/chenxiaolong/zip",
|
||||
]
|
||||
|
||||
+4
-1
@@ -9,12 +9,12 @@ publish = false
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
aws-lc-rs = { version = "1.0.0", default-features = false, features = ["aws-lc-sys"] }
|
||||
anyhow = "1.0.75"
|
||||
avbroot = { path = "../avbroot" }
|
||||
clap = { version = "4.4.1", features = ["derive"] }
|
||||
ctrlc = "3.4.0"
|
||||
hex = { version = "0.4.3", features = ["serde"] }
|
||||
ring = "0.17.0"
|
||||
rsa = { version = "0.9.6", features = ["hazmat"] }
|
||||
serde = { version = "1.0.188", features = ["derive"] }
|
||||
tempfile = "3.8.0"
|
||||
@@ -32,3 +32,6 @@ default-features = false
|
||||
|
||||
[features]
|
||||
static = ["avbroot/static"]
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
+10
-10
@@ -50,11 +50,11 @@ data.ramdisks = [["otacerts", "first_stage", "dsu_key_dir"]]
|
||||
|
||||
[profile.pixel_v4_gki.hashes_streaming]
|
||||
original = "c00f891f941f3dddb28966f7b07f3acea773bee104dace82b37c2d1341f09422"
|
||||
patched = "ce9d8ee97828d233809742a5d3f23aa27b042675b1935ca9e3df0592c55788fd"
|
||||
patched = "6c27ffb07f4497af8539f8283e506066af9417580230c3209c9875fc15d5069d"
|
||||
|
||||
[profile.pixel_v4_gki.hashes_seekable]
|
||||
original = "96a6c366b5de1c3b10d4d6cb4ca503c83ac4cd9ca952a965cceb041990ba7022"
|
||||
patched = "e7b4609ba7a23609211dcae143bc43f091f286fbbb3a9301c02ee25614d35deb"
|
||||
patched = "e4fc12523ffc312796b92210bc1e3bbb70dd60797a47daae76c8b5852e48b382"
|
||||
|
||||
# Google Pixel 6a
|
||||
# What's unique: boot (boot v4, no ramdisk) + vendor_boot (vendor v4, 2 ramdisks)
|
||||
@@ -91,11 +91,11 @@ data.ramdisks = [["init", "otacerts", "first_stage", "dsu_key_dir"], ["dlkm"]]
|
||||
|
||||
[profile.pixel_v4_non_gki.hashes_streaming]
|
||||
original = "4d692bc777b568b0626d3c08d2e6f83f1b472db5ad903486daaec6a78d0cc26e"
|
||||
patched = "e27673e4f30933710c11d51f0e73849068cbe9bc9f54e6076bdd93f9a5c8ea0a"
|
||||
patched = "6832ded3e98a14edc8c5ea7284fcea0b958fa710ebf222c27116faec8dfefe2e"
|
||||
|
||||
[profile.pixel_v4_non_gki.hashes_seekable]
|
||||
original = "ea27ecd9718c17b63400b2548680bb3cee93ce63b4fc44ff9654ca0d9c5372a8"
|
||||
patched = "3456b14e014cf565a808a9e834d9105a23539f07b2c460db19c9384aadbc3b93"
|
||||
patched = "114f8936e917d7e4a71bc1521adb3c8e676de3a738f8c7c505b86464d20bd95c"
|
||||
|
||||
# Google Pixel 4a 5G
|
||||
# What's unique: boot (boot v3) + vendor_boot (vendor v3)
|
||||
@@ -133,11 +133,11 @@ data.ramdisks = [["otacerts", "first_stage", "dsu_key_dir"]]
|
||||
|
||||
[profile.pixel_v3.hashes_streaming]
|
||||
original = "f432dc7931520feb238474aa707dd5299747562ffe6129f3f763b5f11ac473ab"
|
||||
patched = "3850a2e73bd783a1ec4a70c59f37d2374e017c20df7ab4b591182b14d187c18e"
|
||||
patched = "1f28d9210a17e233cd5da4af55b07db764b19eeab991394514170b405240464f"
|
||||
|
||||
[profile.pixel_v3.hashes_seekable]
|
||||
original = "7d29ecc6780953c22052a576b8dc85066c8667a875e918a786a08ff4545b47d1"
|
||||
patched = "9f6342940b7cfbeb27b0567f006bb35cbee910ef038ec535403c662d5252ca71"
|
||||
patched = "27b80c7be9c1e527ea26abe3dabde245c580e6f26ec084204278fbfd81a39f83"
|
||||
|
||||
# Google Pixel 4a
|
||||
# What's unique: boot (boot v2)
|
||||
@@ -168,9 +168,9 @@ data.type = "vbmeta"
|
||||
data.deps = ["system"]
|
||||
|
||||
[profile.pixel_v2.hashes_streaming]
|
||||
original = "1b45235b58054009cc496f6c3ee11d3dc16ed5c388c861761e26a6fce83103a0"
|
||||
patched = "193b2dc70dd465d686f35c7b7f74d2cc1b06a55e48cf5c2e4df0f667e03032fc"
|
||||
original = "4b7e5675f834ac56bf3459628adfe1425c2346a1c224ee6ea0a3be9f996db254"
|
||||
patched = "4a7ca99808b4e49a2dc77a620d4fb7d8219b974ab5b3d5b5709084942805dc9a"
|
||||
|
||||
[profile.pixel_v2.hashes_seekable]
|
||||
original = "52284308fae10cbaf09ade14e92f3bbe6149751a42bff15432982fcef8d890ab"
|
||||
patched = "7ad74ac87ddcaf34938017e6149a646041d70926e31ecda93e156e9397467b3b"
|
||||
original = "66b44b148b35a8a998214e0ae36470b42ffd6d2974e18cd8fa12cd8af0c98540"
|
||||
patched = "5ddec4bb56dd78a49fbc49e9ad3de7710b0b008b031ff4dfce8fdc775e753ad1"
|
||||
|
||||
+2
-4
@@ -1,7 +1,5 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2023 Andrew Gunnerson
|
||||
* SPDX-License-Identifier: GPL-3.0-only
|
||||
*/
|
||||
// SPDX-FileCopyrightText: 2023 Andrew Gunnerson
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use std::{ffi::OsString, path::PathBuf};
|
||||
|
||||
|
||||
+2
-4
@@ -1,7 +1,5 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2023-2024 Andrew Gunnerson
|
||||
* SPDX-License-Identifier: GPL-3.0-only
|
||||
*/
|
||||
// SPDX-FileCopyrightText: 2023-2024 Andrew Gunnerson
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use std::{collections::BTreeMap, fs, path::Path};
|
||||
|
||||
|
||||
+8
-8
@@ -1,8 +1,6 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2023-2024 Andrew Gunnerson
|
||||
* SPDX-FileCopyrightText: 2023 Pascal Roeleven
|
||||
* SPDX-License-Identifier: GPL-3.0-only
|
||||
*/
|
||||
// SPDX-FileCopyrightText: 2023-2024 Andrew Gunnerson
|
||||
// SPDX-FileCopyrightText: 2023 Pascal Roeleven
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
mod cli;
|
||||
mod config;
|
||||
@@ -71,7 +69,7 @@ fn hash_file(path: &Path, cancel_signal: &AtomicBool) -> Result<[u8; 32]> {
|
||||
let raw_reader =
|
||||
File::open(path).with_context(|| format!("Failed to open for reading: {path:?}"))?;
|
||||
let buf_reader = BufReader::new(raw_reader);
|
||||
let context = ring::digest::Context::new(&ring::digest::SHA256);
|
||||
let context = aws_lc_rs::digest::Context::new(&aws_lc_rs::digest::SHA256);
|
||||
let mut hashing_reader = HashingReader::new(buf_reader, context);
|
||||
|
||||
stream::copy(&mut hashing_reader, io::sink(), cancel_signal)?;
|
||||
@@ -106,7 +104,7 @@ fn append_avb(
|
||||
cancel_signal: &AtomicBool,
|
||||
) -> Result<()> {
|
||||
let image_size = file.seek(SeekFrom::End(0))?;
|
||||
let salt = ring::digest::digest(&ring::digest::SHA256, b"avbroot");
|
||||
let salt = aws_lc_rs::digest::digest(&aws_lc_rs::digest::SHA256, b"avbroot");
|
||||
let descriptors = vec![
|
||||
if hash_tree {
|
||||
let mut descriptor = HashTreeDescriptor {
|
||||
@@ -627,6 +625,7 @@ fn create_payload(
|
||||
version: None,
|
||||
merge_operations: vec![],
|
||||
estimate_cow_size: cow_estimate,
|
||||
estimate_op_count_max: None,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -650,6 +649,7 @@ fn create_payload(
|
||||
vabc_compression_param: profile.vabc_algo.map(|a| a.to_string()),
|
||||
cow_version: Some(2),
|
||||
vabc_feature_set: None,
|
||||
compression_factor: None,
|
||||
}),
|
||||
partial_update: None,
|
||||
apex_info: vec![],
|
||||
@@ -755,7 +755,7 @@ fn create_ota(
|
||||
|
||||
match path {
|
||||
ota::PATH_OTACERT => {
|
||||
crypto::write_pem_cert(&mut writer, cert_ota)
|
||||
crypto::write_pem_cert(Path::new(path), &mut writer, cert_ota)
|
||||
.with_context(|| format!("Failed to write entry: {path}"))?;
|
||||
}
|
||||
ota::PATH_PAYLOAD => {
|
||||
|
||||
@@ -16,3 +16,6 @@ publish = false
|
||||
[target.'cfg(unix)'.dependencies]
|
||||
avbroot = { path = "../avbroot" }
|
||||
honggfuzz = "0.5.55"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
// SPDX-FileCopyrightText: 2023 Andrew Gunnerson
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
#[cfg(not(windows))]
|
||||
mod fuzz {
|
||||
use std::io::Cursor;
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
// SPDX-FileCopyrightText: 2023 Andrew Gunnerson
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
#[cfg(not(windows))]
|
||||
mod fuzz {
|
||||
use std::io::Cursor;
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
// SPDX-FileCopyrightText: 2023 Andrew Gunnerson
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
#[cfg(not(windows))]
|
||||
mod fuzz {
|
||||
use std::{io::Cursor, sync::atomic::AtomicBool};
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
// SPDX-FileCopyrightText: 2023 Andrew Gunnerson
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
#[cfg(not(windows))]
|
||||
mod fuzz {
|
||||
use std::{io::Cursor, sync::atomic::AtomicBool};
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
// SPDX-FileCopyrightText: 2024 Andrew Gunnerson
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
#[cfg(not(windows))]
|
||||
mod fuzz {
|
||||
use std::io::Cursor;
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
// SPDX-FileCopyrightText: 2024 Andrew Gunnerson
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
#[cfg(not(windows))]
|
||||
mod fuzz {
|
||||
use std::io::{self, Cursor};
|
||||
|
||||
@@ -13,3 +13,6 @@ anyhow = "1.0.75"
|
||||
clap = { version = "4.4.1", features = ["derive"] }
|
||||
regex = { version = "1.9.4", default-features = false, features = ["perf", "std"] }
|
||||
toml_edit = "0.22.9"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2023 Andrew Gunnerson
|
||||
* SPDX-License-Identifier: GPL-3.0-only
|
||||
*/
|
||||
// SPDX-FileCopyrightText: 2023 Andrew Gunnerson
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use std::{
|
||||
collections::BTreeMap,
|
||||
|
||||
+2
-4
@@ -1,7 +1,5 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2023 Andrew Gunnerson
|
||||
* SPDX-License-Identifier: GPL-3.0-only
|
||||
*/
|
||||
// SPDX-FileCopyrightText: 2023 Andrew Gunnerson
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
mod changelog;
|
||||
mod version;
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2023-2024 Andrew Gunnerson
|
||||
* SPDX-License-Identifier: GPL-3.0-only
|
||||
*/
|
||||
// SPDX-FileCopyrightText: 2023-2024 Andrew Gunnerson
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use std::{
|
||||
fs::{self, File},
|
||||
|
||||
Reference in New Issue
Block a user