mirror of
https://github.com/chenxiaolong/avbroot.git
synced 2026-07-03 14:05:11 +02:00
Compare commits
15 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| bc358c62af | |||
| fc05cb901a | |||
| 031ac8aa31 | |||
| 4a1dab4069 | |||
| bf885e40cf | |||
| c2a441cf78 | |||
| f6c6a9509a | |||
| 5a00995366 | |||
| edf537aad6 | |||
| f006f20209 | |||
| f36c1ca451 | |||
| 29b72961a3 | |||
| e105efb6d3 | |||
| 2a7df104ed | |||
| 8e52a9cf8c |
@@ -19,6 +19,7 @@ jobs:
|
||||
# https://github.com/rust-lang/rust/issues/78210
|
||||
RUSTFLAGS: -C strip=symbols -C target-feature=+crt-static
|
||||
TARGETS: ${{ join(matrix.artifact.targets, ' ') || matrix.artifact.name }}
|
||||
ANDROID_API: ${{ matrix.artifact.android_api }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
@@ -33,6 +34,12 @@ jobs:
|
||||
- aarch64-apple-darwin
|
||||
- x86_64-apple-darwin
|
||||
combine: lipo
|
||||
# ubuntu-latest is not 24.04 yet and 22.04's qemu-user-static segfaults.
|
||||
- os: ubuntu-24.04
|
||||
name: aarch64-linux-android31
|
||||
targets:
|
||||
- aarch64-linux-android
|
||||
android_api: '31'
|
||||
steps:
|
||||
- name: Check out repository
|
||||
uses: actions/checkout@v4
|
||||
@@ -40,6 +47,26 @@ jobs:
|
||||
# For git describe
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Install qemu-user-static
|
||||
if: ${{ contains(matrix.artifact.name, 'android') }}
|
||||
shell: bash
|
||||
run: |
|
||||
sudo apt-get -y update
|
||||
sudo apt-get -y install qemu-user-static
|
||||
|
||||
- name: Set Android temporary directory
|
||||
if: ${{ contains(matrix.artifact.name, 'android') }}
|
||||
shell: bash
|
||||
run: |
|
||||
echo "TMPDIR=/tmp" >> "${GITHUB_ENV}"
|
||||
|
||||
- name: Install cargo-android
|
||||
shell: bash
|
||||
run: |
|
||||
cargo install \
|
||||
--git https://github.com/chenxiaolong/cargo-android \
|
||||
--tag v0.1.1
|
||||
|
||||
- name: Get version
|
||||
id: get_version
|
||||
shell: bash
|
||||
@@ -63,7 +90,8 @@ jobs:
|
||||
shell: bash
|
||||
run: |
|
||||
for target in ${TARGETS}; do
|
||||
cargo clippy --release --workspace --features static \
|
||||
cargo android \
|
||||
clippy --release --workspace --features static \
|
||||
--target "${target}"
|
||||
done
|
||||
|
||||
@@ -71,7 +99,8 @@ jobs:
|
||||
shell: bash
|
||||
run: |
|
||||
for target in ${TARGETS}; do
|
||||
cargo build --release --workspace --features static \
|
||||
cargo android \
|
||||
build --release --workspace --features static \
|
||||
--target "${target}"
|
||||
done
|
||||
|
||||
@@ -79,7 +108,8 @@ jobs:
|
||||
shell: bash
|
||||
run: |
|
||||
for target in ${TARGETS}; do
|
||||
cargo test --release --workspace --features static \
|
||||
cargo android \
|
||||
test --release --workspace --features static \
|
||||
--target "${target}"
|
||||
done
|
||||
|
||||
@@ -87,7 +117,8 @@ jobs:
|
||||
shell: bash
|
||||
run: |
|
||||
for target in ${TARGETS}; do
|
||||
cargo run --release -p e2e --features static \
|
||||
cargo android \
|
||||
run --release -p e2e --features static \
|
||||
--target "${target}" \
|
||||
-- test -a -c e2e/e2e.toml
|
||||
done
|
||||
|
||||
@@ -7,6 +7,22 @@
|
||||
to update the actual links at the bottom of the file.
|
||||
-->
|
||||
|
||||
### Version 3.4.0
|
||||
|
||||
* Fix (unreachable) minor error handling logic when attempting to use unsupported AVB signing algorithms ([PR #311])
|
||||
* Add support for performing signing operations with external programs ([Issue #310], [PR #312])
|
||||
* See the linked issue for an example of how to sign with a Yubikey.
|
||||
|
||||
### Version 3.3.0
|
||||
|
||||
* Recompute CoW size estimate when replacing dynamic partitions ([Issue #306], [PR #307])
|
||||
* Fixes out of space error when flashing a patched OTA that uses `--replace` to replace a dynamic partition (eg. `system`) with a larger or more incompressible image
|
||||
* Add `avbroot payload info` subcommand for inspecting `payload.bin` headers ([PR #309])
|
||||
|
||||
### Version 3.2.3
|
||||
|
||||
* Add prebuilt binary for Android (aarch64) ([PR #304])
|
||||
|
||||
### Version 3.2.2
|
||||
|
||||
* Add new `--recompute-size` option to `avbroot avb pack` to automatically recompute the image size for resizable images ([Discussion #294], [PR #296])
|
||||
@@ -207,6 +223,8 @@ Behind-the-scenes changes:
|
||||
[Issue #285]: https://github.com/chenxiaolong/avbroot/issues/285
|
||||
[Issue #291]: https://github.com/chenxiaolong/avbroot/issues/291
|
||||
[Issue #301]: https://github.com/chenxiaolong/avbroot/issues/301
|
||||
[Issue #306]: https://github.com/chenxiaolong/avbroot/issues/306
|
||||
[Issue #310]: https://github.com/chenxiaolong/avbroot/issues/310
|
||||
[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
|
||||
@@ -295,3 +313,8 @@ Behind-the-scenes changes:
|
||||
[PR #293]: https://github.com/chenxiaolong/avbroot/pull/293
|
||||
[PR #296]: https://github.com/chenxiaolong/avbroot/pull/296
|
||||
[PR #297]: https://github.com/chenxiaolong/avbroot/pull/297
|
||||
[PR #304]: https://github.com/chenxiaolong/avbroot/pull/304
|
||||
[PR #307]: https://github.com/chenxiaolong/avbroot/pull/307
|
||||
[PR #309]: https://github.com/chenxiaolong/avbroot/pull/309
|
||||
[PR #311]: https://github.com/chenxiaolong/avbroot/pull/311
|
||||
[PR #312]: https://github.com/chenxiaolong/avbroot/pull/312
|
||||
|
||||
Generated
+4
-4
@@ -109,7 +109,7 @@ checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0"
|
||||
|
||||
[[package]]
|
||||
name = "avbroot"
|
||||
version = "3.2.2"
|
||||
version = "3.4.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"assert_matches",
|
||||
@@ -534,7 +534,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "e2e"
|
||||
version = "3.2.2"
|
||||
version = "3.4.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"avbroot",
|
||||
@@ -628,7 +628,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "fuzz"
|
||||
version = "3.2.2"
|
||||
version = "3.4.0"
|
||||
dependencies = [
|
||||
"avbroot",
|
||||
"honggfuzz",
|
||||
@@ -2068,7 +2068,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "xtask"
|
||||
version = "3.2.2"
|
||||
version = "3.4.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"clap",
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@ members = ["avbroot", "e2e", "fuzz", "xtask"]
|
||||
resolver = "2"
|
||||
|
||||
[workspace.package]
|
||||
version = "3.2.2"
|
||||
version = "3.4.0"
|
||||
license = "GPL-3.0-only"
|
||||
edition = "2021"
|
||||
repository = "https://github.com/chenxiaolong/avbroot"
|
||||
|
||||
@@ -252,3 +252,13 @@ avbroot hash-tree verify -i <input data file> -H <input hash tree file>
|
||||
```
|
||||
|
||||
This will check if the input file has any corrupted blocks. Currently, the command cannot report which specific blocks are corrupted, only whether the file is valid.
|
||||
|
||||
## `avbroot payload`
|
||||
|
||||
### Showing payload header information
|
||||
|
||||
```bash
|
||||
avbroot payload info -i <payload>
|
||||
```
|
||||
|
||||
This subcommand shows all of the payload header fields (which will likely be extremely long).
|
||||
|
||||
@@ -126,7 +126,7 @@ When patching OTAs for multiple devices, generating unique keys for each device
|
||||
avbroot key generate-cert -k ota.key -o ota.crt
|
||||
```
|
||||
|
||||
The commands above are provided for convenience. avbroot is compatible with any standard PKCS8-encoded 4096-bit RSA private key and PEM-encoded X509 certificate, like those generated by openssl.
|
||||
The commands above are provided for convenience. avbroot is compatible with any standard PKCS#8-encoded 4096-bit RSA private key and PEM-encoded X509 certificate, like those generated by openssl.
|
||||
|
||||
If you lose your AVB or OTA signing key, you will no longer be able to sign new OTA zips. You will have to generate new signing keys and unlock your bootloader again (triggering a data wipe). Follow the [Usage section](#usage) as if doing an initial setup.
|
||||
|
||||
@@ -426,6 +426,28 @@ avbroot ota extract \
|
||||
--all
|
||||
```
|
||||
|
||||
### Signing with an external program
|
||||
|
||||
avbroot supports delegating all RSA signing operations to an external program with the `--signing-helper` option. When using this option, the `--key-avb` and `--key-ota` options must be given a public key instead of a private key.
|
||||
|
||||
For each signing operation, avbroot will invoke the program with:
|
||||
|
||||
```bash
|
||||
<helper> <algorithm> <public key>
|
||||
```
|
||||
|
||||
The algorithm is one of `SHA{256,512}_RSA{2048,4096}` and the public key is what was passed to avbroot. The program can use the public key to find the corresponding private key (eg. on a hardware security module). avbroot will write a PKCS#1 v1.5 padded digest to `stdin` and the helper program is expected to perform a raw RSA signing operation and write the raw signature (octet string matching key size) to `stdout`.
|
||||
|
||||
By default, this behavior is compatible with the `--signing_helper` option in AOSP's avbtool. However, avbroot additionally extends the arguments to support non-interactive use. If `--pass-{avb,ota}-file` or `--pass-{avb,ota}-env-var` are used, then the helper program will be invoked with two additional arguments that point to the password file or environment variable.
|
||||
|
||||
```bash
|
||||
<helper> <algorithm> <public key> file <pass file>
|
||||
# or
|
||||
<helper> <algorithm> <public key> env <env file>
|
||||
```
|
||||
|
||||
Note that avbroot will verify the signature returned by helper program against the public key. This ensures that the patching process will fail appropriately if the wrong private key was used.
|
||||
|
||||
## Building from source
|
||||
|
||||
Make sure the [Rust toolchain](https://www.rust-lang.org/) is installed. Then run:
|
||||
@@ -440,6 +462,16 @@ Debug builds work too, but they will run significantly slower (in the sha256 com
|
||||
|
||||
By default, the executable links to the system's bzip2 and liblzma libraries, which are the only external libraries avbroot depends on. To compile and statically link these two libraries, pass in `--features static`.
|
||||
|
||||
### Android cross-compilation
|
||||
|
||||
To cross-compile for Android, install [cargo-android](https://github.com/chenxiaolong/cargo-android) and use the `cargo android` wrapper. To make a release build for aarch64, run:
|
||||
|
||||
```bash
|
||||
cargo android build --release --target aarch64-linux-android
|
||||
```
|
||||
|
||||
It is possible to run the tests if the host is running Linux, qemu-user-static is installed, and the executable is built with `RUSTFLAGS=-C target-feature=+crt-static` and `--features static`.
|
||||
|
||||
## Verifying digital signatures
|
||||
|
||||
First, save the public key to a file listing the keys to be trusted. This is the same key listed in [the author's profile](https://github.com/chenxiaolong/).
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2023 Andrew Gunnerson
|
||||
* SPDX-FileCopyrightText: 2023-2024 Andrew Gunnerson
|
||||
* SPDX-License-Identifier: GPL-3.0-only
|
||||
*/
|
||||
|
||||
@@ -15,7 +15,7 @@ use clap::{Parser, Subcommand, ValueEnum};
|
||||
use tracing::{debug, Level};
|
||||
use tracing_subscriber::fmt::{format::Writer, time::FormatTime};
|
||||
|
||||
use crate::cli::{avb, boot, completion, cpio, fec, hashtree, key, ota};
|
||||
use crate::cli::{avb, boot, completion, cpio, fec, hashtree, key, ota, payload};
|
||||
|
||||
#[allow(clippy::large_enum_variant)]
|
||||
#[derive(Debug, Subcommand)]
|
||||
@@ -28,6 +28,7 @@ pub enum Command {
|
||||
HashTree(hashtree::HashTreeCli),
|
||||
Key(key::KeyCli),
|
||||
Ota(ota::OtaCli),
|
||||
Payload(payload::PayloadCli),
|
||||
/// (Deprecated: Use `avbroot ota patch` instead.)
|
||||
Patch(ota::PatchCli),
|
||||
/// (Deprecated: Use `avbroot ota extract` instead.)
|
||||
@@ -163,6 +164,7 @@ pub fn main(logging_initialized: &AtomicBool, cancel_signal: &AtomicBool) -> Res
|
||||
Command::HashTree(c) => hashtree::hash_tree_main(&c, cancel_signal),
|
||||
Command::Key(c) => key::key_main(&c),
|
||||
Command::Ota(c) => ota::ota_main(&c, cancel_signal),
|
||||
Command::Payload(c) => payload::payload_main(&c),
|
||||
// Deprecated aliases.
|
||||
Command::Patch(c) => ota::patch_subcommand(&c, cancel_signal),
|
||||
Command::Extract(c) => ota::extract_subcommand(&c, cancel_signal),
|
||||
|
||||
+34
-8
@@ -25,7 +25,7 @@ use serde::{Deserialize, Serialize};
|
||||
use tracing::{debug_span, info, warn, Span};
|
||||
|
||||
use crate::{
|
||||
crypto::{self, PassphraseSource},
|
||||
crypto::{self, PassphraseSource, RsaSigningKey},
|
||||
format::avb::{
|
||||
self, AlgorithmType, AppendedDescriptorMut, AppendedDescriptorRef, Descriptor, Footer,
|
||||
HashTreeDescriptor, Header, KernelCmdlineDescriptor,
|
||||
@@ -383,12 +383,26 @@ fn sign_or_clear(info: &mut AvbInfo, orig_header: &Header, key_group: &KeyGroup)
|
||||
key_group.pass_file.as_deref(),
|
||||
key_group.pass_env_var.as_deref(),
|
||||
);
|
||||
let private_key = crypto::read_pem_key_file(key_path, &source)
|
||||
.with_context(|| format!("Failed to load key: {key_path:?}"))?;
|
||||
let signing_key = if let Some(helper) = &key_group.signing_helper {
|
||||
let public_key = crypto::read_pem_public_key_file(key_path)
|
||||
.with_context(|| format!("Failed to load key: {key_path:?}"))?;
|
||||
|
||||
info.header.set_algo_for_key(&private_key)?;
|
||||
RsaSigningKey::External {
|
||||
program: helper.clone(),
|
||||
public_key_file: key_path.clone(),
|
||||
public_key,
|
||||
passphrase_source: source,
|
||||
}
|
||||
} else {
|
||||
let private_key = crypto::read_pem_key_file(key_path, &source)
|
||||
.with_context(|| format!("Failed to load key: {key_path:?}"))?;
|
||||
|
||||
RsaSigningKey::Internal(private_key)
|
||||
};
|
||||
|
||||
info.header.set_algo_for_key(&signing_key)?;
|
||||
info.header
|
||||
.sign(&private_key)
|
||||
.sign(&signing_key)
|
||||
.context("Failed to sign new AVB header")?;
|
||||
}
|
||||
SignAction::Clear => {
|
||||
@@ -743,12 +757,15 @@ struct DisplayGroup {
|
||||
|
||||
#[derive(Debug, Args)]
|
||||
struct KeyGroup {
|
||||
/// Path to private key for signing.
|
||||
/// Path to signing key.
|
||||
///
|
||||
/// A private key is needed if packing an image where the original header
|
||||
/// A signing key is needed if packing an image where the original header
|
||||
/// was signed and the header needs to be modified (eg. for a new checksum).
|
||||
/// If the header was originally not signed, then the private key is not
|
||||
/// If the header was originally not signed, then the signing key is not
|
||||
/// used, unless --force is specified.
|
||||
///
|
||||
/// This should normally be a private key. However, if --signing-helper is
|
||||
/// used, then it should be a public key instead.
|
||||
#[arg(short, long, value_name = "FILE", value_parser)]
|
||||
key: Option<PathBuf>,
|
||||
|
||||
@@ -768,6 +785,15 @@ struct KeyGroup {
|
||||
/// File containing private key passphrase.
|
||||
#[arg(long, value_name = "FILE", value_parser, group = "pass")]
|
||||
pass_file: Option<PathBuf>,
|
||||
|
||||
/// External program for signing.
|
||||
///
|
||||
/// If this option is specified, then --key must refer to a public key. The
|
||||
/// program will be invoked as:
|
||||
///
|
||||
/// <program> <algo> <public key> [file <pass file>|env <pass env>]
|
||||
#[arg(long, value_name = "PROGRAM", value_parser)]
|
||||
signing_helper: Option<PathBuf>,
|
||||
}
|
||||
|
||||
/// Unpack an AVB image.
|
||||
|
||||
@@ -55,6 +55,9 @@ pub fn key_main(cli: &KeyCli) -> Result<()> {
|
||||
.with_context(|| format!("Failed to load key: {p:?}"))?;
|
||||
|
||||
private_key.to_public_key()
|
||||
} else if let Some(p) = &c.input.public_key {
|
||||
crypto::read_pem_public_key_file(p)
|
||||
.with_context(|| format!("Failed to load public key: {p:?}"))?
|
||||
} else if let Some(p) = &c.input.cert {
|
||||
let certificate = crypto::read_pem_cert_file(p)
|
||||
.with_context(|| format!("Failed to load certificate: {p:?}"))?;
|
||||
@@ -93,6 +96,10 @@ struct PublicKeyInputGroup {
|
||||
#[arg(short, long, value_name = "FILE", value_parser)]
|
||||
key: Option<PathBuf>,
|
||||
|
||||
/// Path to public key.
|
||||
#[arg(short, long, value_name = "FILE", value_parser, conflicts_with_all = ["pass_env_var", "pass_file"])]
|
||||
public_key: Option<PathBuf>,
|
||||
|
||||
/// Path to certificate.
|
||||
#[arg(short, long, value_name = "FILE", value_parser, conflicts_with_all = ["pass_env_var", "pass_file"])]
|
||||
cert: Option<PathBuf>,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2023 Andrew Gunnerson
|
||||
* SPDX-FileCopyrightText: 2023-2024 Andrew Gunnerson
|
||||
* SPDX-License-Identifier: GPL-3.0-only
|
||||
*/
|
||||
|
||||
@@ -12,3 +12,4 @@ pub mod fec;
|
||||
pub mod hashtree;
|
||||
pub mod key;
|
||||
pub mod ota;
|
||||
pub mod payload;
|
||||
|
||||
+88
-18
@@ -21,7 +21,6 @@ use cap_std::{ambient_authority, fs::Dir};
|
||||
use cap_tempfile::TempDir;
|
||||
use clap::{value_parser, ArgAction, Args, Parser, Subcommand};
|
||||
use rayon::{iter::IntoParallelRefIterator, prelude::ParallelIterator};
|
||||
use rsa::RsaPrivateKey;
|
||||
use tempfile::NamedTempFile;
|
||||
use topological_sort::TopologicalSort;
|
||||
use tracing::{debug_span, info, warn};
|
||||
@@ -30,7 +29,7 @@ use zip::{write::FileOptions, CompressionMethod, ZipArchive, ZipWriter};
|
||||
|
||||
use crate::{
|
||||
cli,
|
||||
crypto::{self, PassphraseSource},
|
||||
crypto::{self, PassphraseSource, RsaSigningKey},
|
||||
format::{
|
||||
avb::{self, Descriptor, Header},
|
||||
ota::{self, SigningWriter, ZipEntry},
|
||||
@@ -197,7 +196,7 @@ fn patch_boot_images<'a, 'b: 'a>(
|
||||
required_images: &'b RequiredImages,
|
||||
input_files: &mut HashMap<String, InputFile>,
|
||||
boot_patchers: Vec<Box<dyn BootImagePatch + Sync>>,
|
||||
key_avb: &RsaPrivateKey,
|
||||
key_avb: &RsaSigningKey,
|
||||
cancel_signal: &AtomicBool,
|
||||
) -> Result<()> {
|
||||
let input_files = Mutex::new(input_files);
|
||||
@@ -241,7 +240,7 @@ fn patch_system_image<'a, 'b: 'a>(
|
||||
required_images: &'b RequiredImages,
|
||||
input_files: &mut HashMap<String, InputFile>,
|
||||
cert_ota: &Certificate,
|
||||
key_avb: &RsaPrivateKey,
|
||||
key_avb: &RsaSigningKey,
|
||||
cancel_signal: &AtomicBool,
|
||||
) -> Result<(&'b str, Vec<Range<u64>>)> {
|
||||
let Some(target) = required_images.iter_system().next() else {
|
||||
@@ -565,7 +564,7 @@ fn update_vbmeta_headers(
|
||||
headers: &mut HashMap<String, Header>,
|
||||
order: &mut [(String, HashSet<String>)],
|
||||
clear_vbmeta_flags: bool,
|
||||
key: &RsaPrivateKey,
|
||||
key: &RsaSigningKey,
|
||||
block_size: u64,
|
||||
) -> Result<()> {
|
||||
for (name, deps) in order {
|
||||
@@ -674,12 +673,40 @@ fn compress_image(
|
||||
|
||||
info!("Compressing full image: {name}");
|
||||
|
||||
// Otherwise, compress the entire image.
|
||||
let (partition_info, operations) =
|
||||
payload::compress_image(&*file, &writer, name, block_size, cancel_signal)?;
|
||||
// Otherwise, compress the entire image. If VABC is enabled, we need to
|
||||
// update the CoW size estimate or else the CoW block device may run out of
|
||||
// space during flashing.
|
||||
let need_cow = partition.estimate_cow_size.is_some();
|
||||
if need_cow {
|
||||
info!("Needs updated CoW size estimate: {name}");
|
||||
|
||||
// Only CoW v2 + lz4 seems to exist in the wild currently, so that is
|
||||
// all we support.
|
||||
let Some(dpm) = &header.manifest.dynamic_partition_metadata else {
|
||||
bail!("Dynamic partition metadata is missing");
|
||||
};
|
||||
|
||||
if !dpm.vabc_enabled() {
|
||||
bail!("Partition has CoW estimate, but VABC is disabled: {name}");
|
||||
}
|
||||
|
||||
let cow_version = dpm.cow_version();
|
||||
if dpm.cow_version() != 2 {
|
||||
bail!("Unsupported CoW version: {cow_version}");
|
||||
}
|
||||
|
||||
let compression = dpm.vabc_compression_param();
|
||||
if compression != "lz4" {
|
||||
bail!("Unsupported VABC compression: {compression}");
|
||||
}
|
||||
}
|
||||
|
||||
let (partition_info, operations, cow_estimate) =
|
||||
payload::compress_image(&*file, &writer, name, block_size, need_cow, cancel_signal)?;
|
||||
|
||||
partition.new_partition_info = Some(partition_info);
|
||||
partition.operations = operations;
|
||||
partition.estimate_cow_size = cow_estimate;
|
||||
|
||||
*file = writer;
|
||||
|
||||
@@ -694,8 +721,8 @@ fn patch_ota_payload(
|
||||
external_images: &HashMap<String, PathBuf>,
|
||||
boot_patchers: Vec<Box<dyn BootImagePatch + Sync>>,
|
||||
clear_vbmeta_flags: bool,
|
||||
key_avb: &RsaPrivateKey,
|
||||
key_ota: &RsaPrivateKey,
|
||||
key_avb: &RsaSigningKey,
|
||||
key_ota: &RsaSigningKey,
|
||||
cert_ota: &Certificate,
|
||||
cancel_signal: &AtomicBool,
|
||||
) -> Result<(String, u64)> {
|
||||
@@ -888,8 +915,8 @@ fn patch_ota_zip(
|
||||
external_images: &HashMap<String, PathBuf>,
|
||||
mut boot_patchers: Vec<Box<dyn BootImagePatch + Sync>>,
|
||||
clear_vbmeta_flags: bool,
|
||||
key_avb: &RsaPrivateKey,
|
||||
key_ota: &RsaPrivateKey,
|
||||
key_avb: &RsaSigningKey,
|
||||
key_ota: &RsaSigningKey,
|
||||
cert_ota: &Certificate,
|
||||
cancel_signal: &AtomicBool,
|
||||
) -> Result<(OtaMetadata, u64)> {
|
||||
@@ -1190,10 +1217,38 @@ pub fn patch_subcommand(cli: &PatchCli, cancel_signal: &AtomicBool) -> Result<()
|
||||
cli.pass_ota_env_var.as_deref(),
|
||||
);
|
||||
|
||||
let key_avb = crypto::read_pem_key_file(&cli.key_avb, &source_avb)
|
||||
.with_context(|| format!("Failed to load key: {:?}", cli.key_avb))?;
|
||||
let key_ota = crypto::read_pem_key_file(&cli.key_ota, &source_ota)
|
||||
.with_context(|| format!("Failed to load key: {:?}", cli.key_ota))?;
|
||||
let (key_avb, key_ota) = if let Some(helper) = &cli.signing_helper {
|
||||
let public_key_avb = crypto::read_pem_public_key_file(&cli.key_avb)
|
||||
.with_context(|| format!("Failed to load key: {:?}", cli.key_avb))?;
|
||||
let public_key_ota = crypto::read_pem_public_key_file(&cli.key_ota)
|
||||
.with_context(|| format!("Failed to load key: {:?}", cli.key_ota))?;
|
||||
|
||||
let key_avb = RsaSigningKey::External {
|
||||
program: helper.clone(),
|
||||
public_key_file: cli.key_avb.clone(),
|
||||
public_key: public_key_avb,
|
||||
passphrase_source: source_avb,
|
||||
};
|
||||
let key_ota = RsaSigningKey::External {
|
||||
program: helper.clone(),
|
||||
public_key_file: cli.key_ota.clone(),
|
||||
public_key: public_key_ota,
|
||||
passphrase_source: source_ota,
|
||||
};
|
||||
|
||||
(key_avb, key_ota)
|
||||
} else {
|
||||
let private_key_avb = crypto::read_pem_key_file(&cli.key_avb, &source_avb)
|
||||
.with_context(|| format!("Failed to load key: {:?}", cli.key_avb))?;
|
||||
let private_key_ota = crypto::read_pem_key_file(&cli.key_ota, &source_ota)
|
||||
.with_context(|| format!("Failed to load key: {:?}", cli.key_ota))?;
|
||||
|
||||
let key_avb = RsaSigningKey::Internal(private_key_avb);
|
||||
let key_ota = RsaSigningKey::Internal(private_key_ota);
|
||||
|
||||
(key_avb, key_ota)
|
||||
};
|
||||
|
||||
let cert_ota = crypto::read_pem_cert_file(&cli.cert_ota)
|
||||
.with_context(|| format!("Failed to load certificate: {:?}", cli.cert_ota))?;
|
||||
|
||||
@@ -1720,7 +1775,10 @@ pub struct PatchCli {
|
||||
#[arg(short, long, value_name = "FILE", value_parser, help_heading = HEADING_PATH)]
|
||||
pub output: Option<PathBuf>,
|
||||
|
||||
/// Private key for signing vbmeta images.
|
||||
/// Signing key for vbmeta headers.
|
||||
///
|
||||
/// This should normally be a private key. However, if --signing-helper is
|
||||
/// used, then it should be a public key instead.
|
||||
#[arg(
|
||||
long,
|
||||
alias = "privkey-avb",
|
||||
@@ -1730,7 +1788,10 @@ pub struct PatchCli {
|
||||
)]
|
||||
pub key_avb: PathBuf,
|
||||
|
||||
/// Private key for signing the OTA.
|
||||
/// Signing key for the OTA.
|
||||
///
|
||||
/// This should normally be a private key. However, if --signing-helper is
|
||||
/// used, then it should be a public key instead.
|
||||
#[arg(
|
||||
long,
|
||||
alias = "privkey-ota",
|
||||
@@ -1788,6 +1849,15 @@ pub struct PatchCli {
|
||||
)]
|
||||
pub pass_ota_file: Option<PathBuf>,
|
||||
|
||||
/// External program for signing.
|
||||
///
|
||||
/// If this option is specified, then --key-avb and --key-ota must refer to
|
||||
/// public keys. The program will be invoked as:
|
||||
///
|
||||
/// <program> <algo> <public key> [file <pass file>|env <pass env>]
|
||||
#[arg(long, value_name = "PROGRAM", value_parser, help_heading = HEADING_KEY)]
|
||||
pub signing_helper: Option<PathBuf>,
|
||||
|
||||
/// Use partition image from a file instead of the original payload.
|
||||
#[arg(
|
||||
long,
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2024 Andrew Gunnerson
|
||||
* SPDX-License-Identifier: GPL-3.0-only
|
||||
*/
|
||||
|
||||
use std::{fs::File, io::BufReader, path::PathBuf};
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use clap::{Parser, Subcommand};
|
||||
|
||||
use crate::{format::payload::PayloadHeader, stream::FromReader};
|
||||
|
||||
fn info_subcommand(cli: &InfoCli) -> Result<()> {
|
||||
let mut reader = File::open(&cli.input)
|
||||
.map(BufReader::new)
|
||||
.with_context(|| format!("Failed to open payload: {:?}", cli.input))?;
|
||||
let header = PayloadHeader::from_reader(&mut reader)
|
||||
.with_context(|| format!("Failed to read payload: {:?}", cli.input))?;
|
||||
|
||||
println!("{header:#?}");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn payload_main(cli: &PayloadCli) -> Result<()> {
|
||||
match &cli.command {
|
||||
PayloadCommand::Info(c) => info_subcommand(c),
|
||||
}
|
||||
}
|
||||
|
||||
/// Display payload information.
|
||||
#[derive(Debug, Parser)]
|
||||
struct InfoCli {
|
||||
/// Path to input payload file.
|
||||
#[arg(short, long, value_name = "FILE", value_parser)]
|
||||
input: PathBuf,
|
||||
}
|
||||
|
||||
#[derive(Debug, Subcommand)]
|
||||
enum PayloadCommand {
|
||||
Info(InfoCli),
|
||||
}
|
||||
|
||||
/// Inspect OTA payloads.
|
||||
#[derive(Debug, Parser)]
|
||||
pub struct PayloadCli {
|
||||
#[command(subcommand)]
|
||||
command: PayloadCommand,
|
||||
}
|
||||
+252
-9
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2023 Andrew Gunnerson
|
||||
* SPDX-FileCopyrightText: 2023-2024 Andrew Gunnerson
|
||||
* SPDX-License-Identifier: GPL-3.0-only
|
||||
*/
|
||||
|
||||
@@ -9,6 +9,7 @@ use std::{
|
||||
fs::{self, File, OpenOptions},
|
||||
io::{self, BufReader, BufWriter, Read, Write},
|
||||
path::{Path, PathBuf},
|
||||
process::{Command, ExitStatus, Stdio},
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
@@ -22,12 +23,16 @@ use cms::{
|
||||
};
|
||||
use pkcs8::{
|
||||
pkcs5::{pbes2, scrypt},
|
||||
DecodePrivateKey, EncodePrivateKey, EncodePublicKey, EncryptedPrivateKeyInfo, LineEnding,
|
||||
PrivateKeyInfo,
|
||||
DecodePrivateKey, DecodePublicKey, EncodePrivateKey, EncodePublicKey, EncryptedPrivateKeyInfo,
|
||||
LineEnding, PrivateKeyInfo,
|
||||
};
|
||||
use rand::RngCore;
|
||||
use rsa::{pkcs1v15::SigningKey, Pkcs1v15Sign, RsaPrivateKey, RsaPublicKey};
|
||||
use sha2::Sha256;
|
||||
use rsa::{
|
||||
pkcs1v15::SigningKey, traits::PublicKeyParts, Pkcs1v15Sign, RsaPrivateKey, RsaPublicKey,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sha1::Sha1;
|
||||
use sha2::{Digest, Sha256, Sha512};
|
||||
use thiserror::Error;
|
||||
use x509_cert::{
|
||||
builder::{Builder, CertificateBuilder, Profile},
|
||||
@@ -40,6 +45,20 @@ use x509_cert::{
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum Error {
|
||||
#[error("Signature algorithm not supported: {0:?}")]
|
||||
UnsupportedAlgorithm(SignatureAlgorithm),
|
||||
#[error("RSA key size ({}) not supported", .0 * 8)]
|
||||
UnsupportedKey(usize),
|
||||
#[error("Invalid digest length ({0} bytes) for {1:?}")]
|
||||
InvalidDigestLength(usize, SignatureAlgorithm),
|
||||
#[error("Invalid signature length ({0} bytes) for {1:?}")]
|
||||
InvalidSignatureLength(usize, SignatureAlgorithm),
|
||||
#[error("Failed to run command: {0}")]
|
||||
CommandSpawnFailed(String, #[source] io::Error),
|
||||
#[error("Command failed with status: {1}: {0}")]
|
||||
CommandExecutionFailed(String, ExitStatus),
|
||||
#[error("Signature from signing helper does not match public key: {0:?}")]
|
||||
SigningHelperBadSignature(PathBuf),
|
||||
#[error("Passphrases do not match")]
|
||||
ConfirmPassphrase,
|
||||
#[error("Failed to read environment variable: {0:?}")]
|
||||
@@ -54,6 +73,10 @@ pub enum Error {
|
||||
SaveKeyEncrypted(#[source] pkcs8::Error),
|
||||
#[error("Failed to save unencrypted private key")]
|
||||
SaveKeyUnencrypted(#[source] pkcs8::Error),
|
||||
#[error("Failed to RSA sign digest")]
|
||||
RsaSign(#[source] rsa::Error),
|
||||
#[error("Failed to RSA verify signature")]
|
||||
RsaVerify(#[source] rsa::Error),
|
||||
#[error("X509 error")]
|
||||
X509(#[from] x509_cert::builder::Error),
|
||||
#[error("SPKI error")]
|
||||
@@ -68,6 +91,34 @@ pub enum Error {
|
||||
|
||||
type Result<T> = std::result::Result<T, Error>;
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq, Deserialize, Serialize)]
|
||||
pub enum SignatureAlgorithm {
|
||||
Sha1WithRsa,
|
||||
Sha256WithRsa,
|
||||
Sha512WithRsa,
|
||||
}
|
||||
|
||||
impl SignatureAlgorithm {
|
||||
/// Length of digest required by the signing algorithm.
|
||||
pub fn digest_len(self) -> usize {
|
||||
match self {
|
||||
Self::Sha1WithRsa => Sha1::output_size(),
|
||||
Self::Sha256WithRsa => Sha256::output_size(),
|
||||
Self::Sha512WithRsa => Sha512::output_size(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Compute the digest of the specified data.
|
||||
pub fn hash(self, data: &[u8]) -> Vec<u8> {
|
||||
match self {
|
||||
Self::Sha1WithRsa => Sha1::digest(data).to_vec(),
|
||||
Self::Sha256WithRsa => Sha256::digest(data).to_vec(),
|
||||
Self::Sha512WithRsa => Sha512::digest(data).to_vec(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub enum PassphraseSource {
|
||||
Prompt(String),
|
||||
EnvVar(OsString),
|
||||
@@ -110,6 +161,181 @@ impl PassphraseSource {
|
||||
}
|
||||
}
|
||||
|
||||
fn check_key_size(size: usize) -> Result<()> {
|
||||
// RustCrypto does not support 8192-bit keys.
|
||||
if size > 4096 / 8 {
|
||||
return Err(Error::UnsupportedKey(size));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Copied from rsa-0.9.6 since the function is not exported.
|
||||
fn pkcs1v15_sign_pad(prefix: &[u8], hashed: &[u8], k: usize) -> rsa::Result<Vec<u8>> {
|
||||
let hash_len = hashed.len();
|
||||
let t_len = prefix.len() + hashed.len();
|
||||
if k < t_len + 11 {
|
||||
return Err(rsa::Error::MessageTooLong);
|
||||
}
|
||||
|
||||
// EM = 0x00 || 0x01 || PS || 0x00 || T
|
||||
let mut em = vec![0xff; k];
|
||||
em[0] = 0;
|
||||
em[1] = 1;
|
||||
em[k - t_len - 1] = 0;
|
||||
em[k - t_len..k - hash_len].copy_from_slice(prefix);
|
||||
em[k - hash_len..k].copy_from_slice(hashed);
|
||||
|
||||
Ok(em)
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub enum RsaSigningKey {
|
||||
Internal(RsaPrivateKey),
|
||||
External {
|
||||
program: PathBuf,
|
||||
public_key_file: PathBuf,
|
||||
public_key: RsaPublicKey,
|
||||
passphrase_source: PassphraseSource,
|
||||
},
|
||||
}
|
||||
|
||||
impl RsaSigningKey {
|
||||
/// Size of key in bytes.
|
||||
pub fn size(&self) -> usize {
|
||||
match self {
|
||||
Self::Internal(key) => key.size(),
|
||||
Self::External { public_key, .. } => public_key.size(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the public key portion of the signing key.
|
||||
pub fn to_public_key(&self) -> RsaPublicKey {
|
||||
match self {
|
||||
RsaSigningKey::Internal(key) => key.to_public_key(),
|
||||
RsaSigningKey::External { public_key, .. } => public_key.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Sign the digest with the specified signature algorithm.
|
||||
pub fn sign(&self, algo: SignatureAlgorithm, digest: &[u8]) -> Result<Vec<u8>> {
|
||||
if digest.len() != algo.digest_len() {
|
||||
return Err(Error::InvalidDigestLength(digest.len(), algo));
|
||||
}
|
||||
|
||||
check_key_size(self.size())?;
|
||||
|
||||
let scheme = match algo {
|
||||
// We don't support signing with insecure algorithms.
|
||||
SignatureAlgorithm::Sha1WithRsa => return Err(Error::UnsupportedAlgorithm(algo)),
|
||||
SignatureAlgorithm::Sha256WithRsa => Pkcs1v15Sign::new::<Sha256>(),
|
||||
SignatureAlgorithm::Sha512WithRsa => Pkcs1v15Sign::new::<Sha512>(),
|
||||
};
|
||||
|
||||
match self {
|
||||
Self::Internal(key) => key.sign(scheme, digest).map_err(Error::RsaSign),
|
||||
Self::External {
|
||||
program,
|
||||
public_key,
|
||||
public_key_file,
|
||||
passphrase_source,
|
||||
} => {
|
||||
let key_bits = public_key.size() * 8;
|
||||
let algo_str = match algo {
|
||||
SignatureAlgorithm::Sha1WithRsa => unreachable!(),
|
||||
SignatureAlgorithm::Sha256WithRsa => format!("SHA256_RSA{key_bits}"),
|
||||
SignatureAlgorithm::Sha512WithRsa => format!("SHA512_RSA{key_bits}"),
|
||||
};
|
||||
|
||||
let mut command = Command::new(program);
|
||||
command.arg(algo_str);
|
||||
command.arg(public_key_file);
|
||||
|
||||
match passphrase_source {
|
||||
PassphraseSource::Prompt(_) => {}
|
||||
PassphraseSource::EnvVar(v) => {
|
||||
command.arg("env");
|
||||
command.arg(v);
|
||||
}
|
||||
PassphraseSource::File(p) => {
|
||||
command.arg("file");
|
||||
command.arg(p);
|
||||
}
|
||||
}
|
||||
|
||||
command.stdin(Stdio::piped());
|
||||
command.stdout(Stdio::piped());
|
||||
command.stderr(Stdio::inherit());
|
||||
|
||||
let mut child = command
|
||||
.spawn()
|
||||
.map_err(|e| Error::CommandSpawnFailed(format!("{command:?}"), e))?;
|
||||
|
||||
// We don't bother with spawning a thread. The pipe capacity on
|
||||
// all major OSs is significantly larger than the digest, so we
|
||||
// don't risk deadlocking even if the process doesn't read from
|
||||
// stdin.
|
||||
//
|
||||
// Pipe capacities:
|
||||
// * Linux: 64 KiB
|
||||
// * macOS: 4 KiB, 16 KiB (usually), or 64 KiB
|
||||
// * Windows: 4 KiB
|
||||
|
||||
let padded_digest = pkcs1v15_sign_pad(&scheme.prefix, digest, public_key.size())?;
|
||||
child.stdin.as_mut().unwrap().write_all(&padded_digest)?;
|
||||
|
||||
let child = child.wait_with_output()?;
|
||||
|
||||
if !child.status.success() {
|
||||
return Err(Error::CommandExecutionFailed(
|
||||
format!("{command:?}"),
|
||||
child.status,
|
||||
));
|
||||
} else if child.stdout.len() != self.size() {
|
||||
return Err(Error::InvalidSignatureLength(child.stdout.len(), algo));
|
||||
}
|
||||
|
||||
// Check that the helper signed with the proper key.
|
||||
if let Err(e) = self.to_public_key().verify_sig(algo, digest, &child.stdout) {
|
||||
return match e {
|
||||
Error::RsaVerify(_) => {
|
||||
Err(Error::SigningHelperBadSignature(public_key_file.clone()))
|
||||
}
|
||||
e => Err(e),
|
||||
};
|
||||
}
|
||||
|
||||
Ok(child.stdout)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub trait RsaPublicKeyExt {
|
||||
fn verify_sig(&self, algo: SignatureAlgorithm, digest: &[u8], signature: &[u8]) -> Result<()>;
|
||||
}
|
||||
|
||||
impl RsaPublicKeyExt for RsaPublicKey {
|
||||
/// Verify the signature against the specified key.
|
||||
fn verify_sig(&self, algo: SignatureAlgorithm, digest: &[u8], signature: &[u8]) -> Result<()> {
|
||||
// Check this explicitly so we can provide a better error message.
|
||||
if digest.len() != algo.digest_len() {
|
||||
return Err(Error::InvalidDigestLength(digest.len(), algo));
|
||||
}
|
||||
|
||||
check_key_size(self.size())?;
|
||||
|
||||
let scheme = match algo {
|
||||
SignatureAlgorithm::Sha1WithRsa => Pkcs1v15Sign::new::<Sha1>(),
|
||||
SignatureAlgorithm::Sha256WithRsa => Pkcs1v15Sign::new::<Sha256>(),
|
||||
SignatureAlgorithm::Sha512WithRsa => Pkcs1v15Sign::new::<Sha512>(),
|
||||
};
|
||||
|
||||
self.verify(scheme, digest, signature)
|
||||
.map_err(Error::RsaVerify)
|
||||
}
|
||||
}
|
||||
|
||||
/// Generate an 4096-bit RSA key pair.
|
||||
pub fn generate_rsa_key_pair() -> Result<RsaPrivateKey> {
|
||||
let mut rng = rand::thread_rng();
|
||||
@@ -228,6 +454,16 @@ pub fn write_pem_cert_file(path: &Path, cert: &Certificate) -> Result<()> {
|
||||
write_pem_cert(writer, cert)
|
||||
}
|
||||
|
||||
/// Read PEM-encoded PKCS8 public key from a reader.
|
||||
pub fn read_pem_public_key(mut reader: impl Read) -> Result<RsaPublicKey> {
|
||||
let mut data = String::new();
|
||||
reader.read_to_string(&mut data)?;
|
||||
|
||||
let key = RsaPublicKey::from_public_key_pem(&data)?;
|
||||
|
||||
Ok(key)
|
||||
}
|
||||
|
||||
/// Write PEM-encoded PKCS8 public key to a writer.
|
||||
pub fn write_pem_public_key(mut writer: impl Write, key: &RsaPublicKey) -> Result<()> {
|
||||
let data = key.to_public_key_pem(LineEnding::LF)?;
|
||||
@@ -237,6 +473,14 @@ pub fn write_pem_public_key(mut writer: impl Write, key: &RsaPublicKey) -> Resul
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Read PEM-encoded PKCS8 public key from a file.
|
||||
pub fn read_pem_public_key_file(path: &Path) -> Result<RsaPublicKey> {
|
||||
let file = File::open(path)?;
|
||||
let reader = BufReader::new(file);
|
||||
|
||||
read_pem_public_key(reader)
|
||||
}
|
||||
|
||||
/// Write PEM-encoded PKCS8 public key to a file.
|
||||
pub fn write_pem_public_key_file(path: &Path, key: &RsaPublicKey) -> Result<()> {
|
||||
let file = File::create(path)?;
|
||||
@@ -351,7 +595,7 @@ pub fn get_public_key(cert: &Certificate) -> Result<RsaPublicKey> {
|
||||
}
|
||||
|
||||
/// Check if a certificate matches a private key.
|
||||
pub fn cert_matches_key(cert: &Certificate, key: &RsaPrivateKey) -> Result<bool> {
|
||||
pub fn cert_matches_key(cert: &Certificate, key: &RsaSigningKey) -> Result<bool> {
|
||||
let public_key = get_public_key(cert)?;
|
||||
|
||||
Ok(key.to_public_key() == public_key)
|
||||
@@ -389,12 +633,11 @@ pub fn get_cms_certs(sd: &SignedData) -> Vec<Certificate> {
|
||||
/// a transport mechanism for a raw signature. Thus, we need to ensure that the
|
||||
/// signature covers nothing but the raw data.
|
||||
pub fn cms_sign_external(
|
||||
key: &RsaPrivateKey,
|
||||
key: &RsaSigningKey,
|
||||
cert: &Certificate,
|
||||
digest: &[u8],
|
||||
) -> Result<ContentInfo> {
|
||||
let scheme = Pkcs1v15Sign::new::<Sha256>();
|
||||
let signature = key.sign(scheme, digest)?;
|
||||
let signature = key.sign(SignatureAlgorithm::Sha256WithRsa, digest)?;
|
||||
|
||||
let digest_algorithm = AlgorithmIdentifierOwned {
|
||||
oid: const_oid::db::rfc5912::ID_SHA_256,
|
||||
|
||||
+56
-81
@@ -16,12 +16,12 @@ use byteorder::{BigEndian, ReadBytesExt, WriteBytesExt};
|
||||
use num_bigint_dig::{ModInverse, ToBigInt};
|
||||
use num_traits::{Pow, ToPrimitive};
|
||||
use ring::digest::{Algorithm, Context};
|
||||
use rsa::{traits::PublicKeyParts, BigUint, Pkcs1v15Sign, RsaPrivateKey, RsaPublicKey};
|
||||
use rsa::{traits::PublicKeyParts, BigUint, RsaPublicKey};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sha2::{Digest, Sha256, Sha512};
|
||||
use thiserror::Error;
|
||||
|
||||
use crate::{
|
||||
crypto::{self, RsaPublicKeyExt, RsaSigningKey, SignatureAlgorithm},
|
||||
escape,
|
||||
format::{
|
||||
fec::{self, Fec},
|
||||
@@ -103,12 +103,9 @@ pub enum Error {
|
||||
UnsupportedAlgorithm(AlgorithmType),
|
||||
#[error("Hashing algorithm not supported: {0:?}")]
|
||||
UnsupportedHashAlgorithm(String),
|
||||
#[error("Incorrect key size ({key_size} bytes) for algorithm {algo:?} ({} bytes)", algo.public_key_len())]
|
||||
IncorrectKeySize {
|
||||
key_size: usize,
|
||||
algo: AlgorithmType,
|
||||
},
|
||||
#[error("RSA key size (0) is not compatible with any AVB signing algorithm")]
|
||||
#[error("Incorrect key size ({}) for algorithm {1:?}", .0 * 8)]
|
||||
IncorrectKeySize(usize, AlgorithmType),
|
||||
#[error("RSA key size ({}) is not compatible with any AVB signing algorithm", .0 * 8)]
|
||||
UnsupportedKey(usize),
|
||||
#[error("Hash tree does not immediately follow image data")]
|
||||
HashTreeGap,
|
||||
@@ -120,18 +117,18 @@ pub enum Error {
|
||||
MismatchedFecBlockSizes { data: u32, hash: u32 },
|
||||
#[error("Must have exactly one hash or hash tree descriptor")]
|
||||
NoAppendedDescriptor,
|
||||
#[error("Failed to RSA sign digest")]
|
||||
RsaSign(#[source] rsa::Error),
|
||||
#[error("Failed to RSA verify signature")]
|
||||
RsaVerify(#[source] rsa::Error),
|
||||
#[error("{0} byte image size is too small to fit header")]
|
||||
TooSmallForHeader(u64),
|
||||
#[error("{0} byte image size is too small to fit footer")]
|
||||
TooSmallForFooter(u64),
|
||||
#[error("Crypto error")]
|
||||
Crypto(#[from] crypto::Error),
|
||||
#[error("Hash tree error")]
|
||||
HashTree(#[from] hashtree::Error),
|
||||
#[error("FEC error")]
|
||||
Fec(#[from] fec::Error),
|
||||
#[error("RSA error")]
|
||||
Rsa(#[from] rsa::Error),
|
||||
#[error("I/O error")]
|
||||
Io(#[from] io::Error),
|
||||
}
|
||||
@@ -156,6 +153,7 @@ pub enum AlgorithmType {
|
||||
Sha512Rsa2048,
|
||||
Sha512Rsa4096,
|
||||
Sha512Rsa8192,
|
||||
#[serde(untagged)]
|
||||
Unknown(u32),
|
||||
}
|
||||
|
||||
@@ -186,18 +184,24 @@ impl AlgorithmType {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn hash_len(self) -> usize {
|
||||
pub fn to_digest_algorithm(self) -> Option<SignatureAlgorithm> {
|
||||
match self {
|
||||
Self::None | Self::Unknown(_) => 0,
|
||||
Self::Sha256Rsa2048 | Self::Sha256Rsa4096 | Self::Sha256Rsa8192 => {
|
||||
Sha256::output_size()
|
||||
Some(SignatureAlgorithm::Sha256WithRsa)
|
||||
}
|
||||
Self::Sha512Rsa2048 | Self::Sha512Rsa4096 | Self::Sha512Rsa8192 => {
|
||||
Sha512::output_size()
|
||||
Some(SignatureAlgorithm::Sha512WithRsa)
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn digest_len(self) -> usize {
|
||||
self.to_digest_algorithm()
|
||||
.map(|a| a.digest_len())
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
pub fn signature_len(self) -> usize {
|
||||
match self {
|
||||
Self::None | Self::Unknown(_) => 0,
|
||||
@@ -217,49 +221,36 @@ impl AlgorithmType {
|
||||
}
|
||||
|
||||
pub fn hash(self, data: &[u8]) -> Vec<u8> {
|
||||
match self {
|
||||
Self::None | Self::Unknown(_) => vec![],
|
||||
Self::Sha256Rsa2048 | Self::Sha256Rsa4096 | Self::Sha256Rsa8192 => {
|
||||
Sha256::digest(data).to_vec()
|
||||
}
|
||||
Self::Sha512Rsa2048 | Self::Sha512Rsa4096 | Self::Sha512Rsa8192 => {
|
||||
Sha512::digest(data).to_vec()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn sign(self, key: &RsaPrivateKey, digest: &[u8]) -> Result<Vec<u8>> {
|
||||
let signature = match self {
|
||||
Self::None | Self::Unknown(_) => vec![],
|
||||
Self::Sha256Rsa2048 | Self::Sha256Rsa4096 | Self::Sha256Rsa8192 => {
|
||||
let scheme = Pkcs1v15Sign::new::<Sha256>();
|
||||
key.sign(scheme, digest).map_err(Error::RsaSign)?
|
||||
}
|
||||
Self::Sha512Rsa2048 | Self::Sha512Rsa4096 | Self::Sha512Rsa8192 => {
|
||||
let scheme = Pkcs1v15Sign::new::<Sha512>();
|
||||
key.sign(scheme, digest).map_err(Error::RsaSign)?
|
||||
}
|
||||
let Some(algo) = self.to_digest_algorithm() else {
|
||||
return vec![];
|
||||
};
|
||||
|
||||
Ok(signature)
|
||||
algo.hash(data)
|
||||
}
|
||||
|
||||
pub fn sign(self, key: &RsaSigningKey, digest: &[u8]) -> Result<Vec<u8>> {
|
||||
let Some(algo) = self.to_digest_algorithm() else {
|
||||
return if self == Self::None {
|
||||
Ok(vec![])
|
||||
} else {
|
||||
Err(Error::UnsupportedAlgorithm(self))
|
||||
};
|
||||
};
|
||||
|
||||
key.sign(algo, digest).map_err(|e| e.into())
|
||||
}
|
||||
|
||||
pub fn verify(self, key: &RsaPublicKey, digest: &[u8], signature: &[u8]) -> Result<()> {
|
||||
match self {
|
||||
Self::None | Self::Unknown(_) => {}
|
||||
Self::Sha256Rsa2048 | Self::Sha256Rsa4096 | Self::Sha256Rsa8192 => {
|
||||
let scheme = Pkcs1v15Sign::new::<Sha256>();
|
||||
key.verify(scheme, digest, signature)
|
||||
.map_err(Error::RsaVerify)?;
|
||||
}
|
||||
Self::Sha512Rsa2048 | Self::Sha512Rsa4096 | Self::Sha512Rsa8192 => {
|
||||
let scheme = Pkcs1v15Sign::new::<Sha512>();
|
||||
key.verify(scheme, digest, signature)
|
||||
.map_err(Error::RsaVerify)?;
|
||||
}
|
||||
}
|
||||
let Some(algo) = self.to_digest_algorithm() else {
|
||||
return if self == Self::None {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(Error::UnsupportedAlgorithm(self))
|
||||
};
|
||||
};
|
||||
|
||||
Ok(())
|
||||
key.verify_sig(algo, digest, signature)
|
||||
.map_err(|e| e.into())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1461,7 +1452,7 @@ impl Header {
|
||||
result.ok_or(Error::NoAppendedDescriptor)
|
||||
}
|
||||
|
||||
pub fn set_algo_for_key(&mut self, key: &RsaPrivateKey) -> Result<()> {
|
||||
pub fn set_algo_for_key(&mut self, key: &RsaSigningKey) -> Result<()> {
|
||||
let key_raw = encode_public_key(&key.to_public_key())?;
|
||||
|
||||
for algo in [AlgorithmType::Sha256Rsa2048, AlgorithmType::Sha256Rsa4096] {
|
||||
@@ -1481,30 +1472,17 @@ impl Header {
|
||||
self.public_key_metadata.clear();
|
||||
}
|
||||
|
||||
pub fn sign(&mut self, key: &RsaPrivateKey) -> Result<()> {
|
||||
pub fn sign(&mut self, key: &RsaSigningKey) -> Result<()> {
|
||||
let key_raw = encode_public_key(&key.to_public_key())?;
|
||||
|
||||
// RustCrypto does not support 8192-bit keys.
|
||||
match self.algorithm_type {
|
||||
AlgorithmType::Sha256Rsa8192
|
||||
| AlgorithmType::Sha512Rsa8192
|
||||
| AlgorithmType::Unknown(_) => {
|
||||
return Err(Error::UnsupportedAlgorithm(self.algorithm_type));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
if key_raw.len() != self.algorithm_type.public_key_len() {
|
||||
return Err(Error::IncorrectKeySize {
|
||||
key_size: key_raw.len(),
|
||||
algo: self.algorithm_type,
|
||||
});
|
||||
return Err(Error::IncorrectKeySize(key.size(), self.algorithm_type));
|
||||
}
|
||||
|
||||
// The public key and the sizes of the hash and signature are included
|
||||
// in the data that's about to be signed.
|
||||
self.public_key = key_raw;
|
||||
self.hash.resize(self.algorithm_type.hash_len(), 0);
|
||||
self.hash.resize(self.algorithm_type.digest_len(), 0);
|
||||
self.signature
|
||||
.resize(self.algorithm_type.signature_len(), 0);
|
||||
|
||||
@@ -1525,18 +1503,16 @@ impl Header {
|
||||
/// and return the public key. If the header is not signed, then `None` is
|
||||
/// returned.
|
||||
pub fn verify(&self) -> Result<Option<RsaPublicKey>> {
|
||||
// RustCrypto does not support 8192-bit keys.
|
||||
match self.algorithm_type {
|
||||
AlgorithmType::None => return Ok(None),
|
||||
a @ AlgorithmType::Sha256Rsa8192
|
||||
| a @ AlgorithmType::Sha512Rsa8192
|
||||
| a @ AlgorithmType::Unknown(_) => return Err(Error::UnsupportedAlgorithm(a)),
|
||||
_ => {}
|
||||
}
|
||||
|
||||
// Reconstruct the public key.
|
||||
let public_key = decode_public_key(&self.public_key)?;
|
||||
|
||||
if self.public_key.len() != self.algorithm_type.public_key_len() {
|
||||
return Err(Error::IncorrectKeySize(
|
||||
public_key.size(),
|
||||
self.algorithm_type,
|
||||
));
|
||||
}
|
||||
|
||||
let mut without_auth_writer = Cursor::new(Vec::new());
|
||||
self.to_writer_internal(&mut without_auth_writer, true)?;
|
||||
let without_auth = without_auth_writer.into_inner();
|
||||
@@ -1825,8 +1801,7 @@ pub fn decode_public_key(data: &[u8]) -> Result<RsaPublicKey> {
|
||||
reader.read_exact(&mut modulus_raw)?;
|
||||
|
||||
let modulus = BigUint::from_bytes_be(&modulus_raw);
|
||||
let public_key =
|
||||
RsaPublicKey::new(modulus, BigUint::from(65537u32)).map_err(Error::RsaVerify)?;
|
||||
let public_key = RsaPublicKey::new(modulus, BigUint::from(65537u32))?;
|
||||
|
||||
Ok(public_key)
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2023 Andrew Gunnerson
|
||||
* SPDX-FileCopyrightText: 2023-2024 Andrew Gunnerson
|
||||
* SPDX-License-Identifier: GPL-3.0-only
|
||||
*/
|
||||
|
||||
@@ -12,11 +12,11 @@ use std::{
|
||||
use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt};
|
||||
use num_traits::ToPrimitive;
|
||||
use ring::digest::Context;
|
||||
use rsa::RsaPrivateKey;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use thiserror::Error;
|
||||
|
||||
use crate::{
|
||||
crypto::RsaSigningKey,
|
||||
format::{
|
||||
avb::{self, Descriptor, Header},
|
||||
padding,
|
||||
@@ -765,7 +765,7 @@ impl BootImageV3Through4 {
|
||||
/// Sign the boot image with a legacy VTS signature. Returns true if the
|
||||
/// image was successfully signed. Returns false if there's no vbmeta
|
||||
/// structure to sign in [`V4Extra::signature`].
|
||||
pub fn sign(&mut self, key: &RsaPrivateKey) -> Result<bool> {
|
||||
pub fn sign(&mut self, key: &RsaSigningKey) -> Result<bool> {
|
||||
let mut context = Context::new(&ring::digest::SHA256);
|
||||
let image_size;
|
||||
|
||||
|
||||
@@ -15,15 +15,12 @@ use const_oid::{db::rfc5912, ObjectIdentifier};
|
||||
use memchr::memmem;
|
||||
use prost::Message;
|
||||
use ring::digest::Context;
|
||||
use rsa::{Pkcs1v15Sign, RsaPrivateKey};
|
||||
use sha1::Sha1;
|
||||
use sha2::Sha256;
|
||||
use thiserror::Error;
|
||||
use x509_cert::{der::Encode, Certificate};
|
||||
use zip::{result::ZipError, write::FileOptions, CompressionMethod, ZipArchive, ZipWriter};
|
||||
|
||||
use crate::{
|
||||
crypto,
|
||||
crypto::{self, RsaPublicKeyExt, RsaSigningKey, SignatureAlgorithm},
|
||||
format::payload::{self, PayloadHeader},
|
||||
protobuf::build::tools::releasetools::{ota_metadata::OtaType, OtaMetadata},
|
||||
stream::{self, FromReader, HashingReader, HashingWriter},
|
||||
@@ -88,8 +85,6 @@ pub enum Error {
|
||||
Spki(#[from] pkcs8::spki::Error),
|
||||
#[error("x509 DER error")]
|
||||
Der(#[from] x509_cert::der::Error),
|
||||
#[error("RSA error")]
|
||||
Rsa(#[from] rsa::Error),
|
||||
#[error("Zip error")]
|
||||
Zip(#[from] ZipError),
|
||||
#[error("I/O error")]
|
||||
@@ -586,12 +581,12 @@ pub fn verify_ota(mut reader: impl Read + Seek, cancel_signal: &AtomicBool) -> R
|
||||
reader.seek(SeekFrom::Start(0))?;
|
||||
|
||||
// We support SHA1 for verification only.
|
||||
let (algorithm, scheme) = if signer.digest_alg.oid == rfc5912::ID_SHA_256 {
|
||||
(&ring::digest::SHA256, Pkcs1v15Sign::new::<Sha256>())
|
||||
let (algorithm, algo) = if signer.digest_alg.oid == rfc5912::ID_SHA_256 {
|
||||
(&ring::digest::SHA256, SignatureAlgorithm::Sha256WithRsa)
|
||||
} else {
|
||||
(
|
||||
&ring::digest::SHA1_FOR_LEGACY_USE_ONLY,
|
||||
Pkcs1v15Sign::new::<Sha1>(),
|
||||
SignatureAlgorithm::Sha1WithRsa,
|
||||
)
|
||||
};
|
||||
|
||||
@@ -603,7 +598,7 @@ pub fn verify_ota(mut reader: impl Read + Seek, cancel_signal: &AtomicBool) -> R
|
||||
let digest = context.finish();
|
||||
|
||||
// Verify the signature against the public key.
|
||||
public_key.verify(scheme, digest.as_ref(), signer.signature.as_bytes())?;
|
||||
public_key.verify_sig(algo, digest.as_ref(), signer.signature.as_bytes())?;
|
||||
|
||||
Ok(cert.clone())
|
||||
}
|
||||
@@ -661,7 +656,7 @@ impl<W: Write> SigningWriter<W> {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn finish(mut self, key: &RsaPrivateKey, cert: &Certificate) -> Result<W> {
|
||||
pub fn finish(mut self, key: &RsaSigningKey, cert: &Certificate) -> Result<W> {
|
||||
if self.used < self.queue.len() {
|
||||
return Err(
|
||||
io::Error::new(io::ErrorKind::InvalidData, "Too small to contain EOCD").into(),
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2022-2023 Andrew Gunnerson
|
||||
* SPDX-FileCopyrightText: 2022-2024 Andrew Gunnerson
|
||||
* SPDX-License-Identifier: GPL-3.0-only
|
||||
*/
|
||||
|
||||
@@ -26,13 +26,11 @@ use rayon::{
|
||||
prelude::{IntoParallelIterator, IntoParallelRefIterator, ParallelIterator},
|
||||
};
|
||||
use ring::digest::{Context, Digest};
|
||||
use rsa::{traits::PublicKeyParts, Pkcs1v15Sign, RsaPrivateKey};
|
||||
use sha2::Sha256;
|
||||
use thiserror::Error;
|
||||
use x509_cert::Certificate;
|
||||
|
||||
use crate::{
|
||||
crypto,
|
||||
crypto::{self, RsaPublicKeyExt, RsaSigningKey, SignatureAlgorithm},
|
||||
protobuf::chromeos_update_engine::{
|
||||
install_operation::Type, signatures::Signature, DeltaArchiveManifest, Extent,
|
||||
InstallOperation, PartitionInfo, PartitionUpdate, Signatures,
|
||||
@@ -100,8 +98,6 @@ pub enum Error {
|
||||
ProtobufDecode(#[from] prost::DecodeError),
|
||||
#[error("XZ stream error")]
|
||||
XzStream(#[from] liblzma::stream::Error),
|
||||
#[error("RSA error")]
|
||||
Rsa(#[from] rsa::Error),
|
||||
#[error("I/O error")]
|
||||
Io(#[from] io::Error),
|
||||
}
|
||||
@@ -175,9 +171,8 @@ impl<R: Read> FromReader<R> for PayloadHeader {
|
||||
|
||||
/// Sign `digest` with `key` and return a [`Signatures`] protobuf struct with
|
||||
/// the signature padded to the maximum size.
|
||||
fn sign_digest(digest: &[u8], key: &RsaPrivateKey) -> Result<Signatures> {
|
||||
let scheme = Pkcs1v15Sign::new::<Sha256>();
|
||||
let mut digest_signed = key.sign(scheme, digest)?;
|
||||
fn sign_digest(digest: &[u8], key: &RsaSigningKey) -> Result<Signatures> {
|
||||
let mut digest_signed = key.sign(SignatureAlgorithm::Sha256WithRsa, digest)?;
|
||||
assert!(
|
||||
digest_signed.len() <= key.size(),
|
||||
"Signature exceeds maximum size",
|
||||
@@ -214,8 +209,7 @@ fn verify_digest(digest: &[u8], signatures: &Signatures, cert: &Certificate) ->
|
||||
};
|
||||
let without_padding = &data[..size as usize];
|
||||
|
||||
let scheme = Pkcs1v15Sign::new::<Sha256>();
|
||||
match public_key.verify(scheme, digest, without_padding) {
|
||||
match public_key.verify_sig(SignatureAlgorithm::Sha256WithRsa, digest, without_padding) {
|
||||
Ok(_) => return Ok(()),
|
||||
Err(e) => last_error = Some(e),
|
||||
}
|
||||
@@ -292,7 +286,7 @@ pub struct PayloadWriter<W: Write> {
|
||||
h_partial: Context,
|
||||
/// Includes signatures (hashes are for properties file).
|
||||
h_full: Context,
|
||||
key: RsaPrivateKey,
|
||||
key: RsaSigningKey,
|
||||
}
|
||||
|
||||
/// Write data to a writer and one or more hashers.
|
||||
@@ -315,7 +309,7 @@ impl<W: Write> PayloadWriter<W> {
|
||||
/// fields are ignored and internally recomputed to guarantee that there are
|
||||
/// no gaps. All partitions' install operation data is written to the blob
|
||||
/// section in order.
|
||||
pub fn new(mut inner: W, mut header: PayloadHeader, key: RsaPrivateKey) -> Result<Self> {
|
||||
pub fn new(mut inner: W, mut header: PayloadHeader, key: RsaSigningKey) -> Result<Self> {
|
||||
let mut blob_size = 0;
|
||||
|
||||
// The blob must contain all data in sequential order with no gaps.
|
||||
@@ -890,6 +884,21 @@ fn compress_chunk(raw_data: &[u8], cancel_signal: &AtomicBool) -> Result<(Vec<u8
|
||||
Ok((data, digest_compressed))
|
||||
}
|
||||
|
||||
fn compress_cow_size(mut raw_data: &[u8], block_size: u32) -> u64 {
|
||||
let mut total = 0;
|
||||
|
||||
while !raw_data.is_empty() {
|
||||
let n = raw_data.len().min(block_size as usize);
|
||||
let compressed = lz4_flex::block::compress(&raw_data[..n]);
|
||||
|
||||
total += compressed.len().min(n) as u64;
|
||||
|
||||
raw_data = &raw_data[n..];
|
||||
}
|
||||
|
||||
total
|
||||
}
|
||||
|
||||
/// Compress the image and return the corresponding information to insert into
|
||||
/// the payload manifest's [`PartitionUpdate`] instance. The uncompressed data
|
||||
/// is split into 2 MiB chunks, which are read and compressed in parallel, and
|
||||
@@ -897,13 +906,21 @@ fn compress_chunk(raw_data: &[u8], cancel_signal: &AtomicBool) -> Result<(Vec<u8
|
||||
/// a corresponding [`InstallOperation`] in the return value. The caller must
|
||||
/// update [`InstallOperation::data_offset`] in each operation manually because
|
||||
/// the initial values are relative to 0.
|
||||
///
|
||||
/// If `need_cow_estimate` is true, the VABC CoW v2 + lz4 size estimate will be
|
||||
/// computed. The caller must update [`PartitionUpdate::estimate_cow_size`] with
|
||||
/// this value or else update_engine may fail to flash the partition due to
|
||||
/// running out of space on the CoW block device. CoW v2 + other algorithms and
|
||||
/// also CoW v3 are currently unsupported because there currently are no known
|
||||
/// OTAs that use those configurations.
|
||||
pub fn compress_image(
|
||||
input: &(dyn ReadSeekReopen + Sync),
|
||||
output: &(dyn WriteSeekReopen + Sync),
|
||||
partition_name: &str,
|
||||
block_size: u32,
|
||||
need_cow_estimate: bool,
|
||||
cancel_signal: &AtomicBool,
|
||||
) -> Result<(PartitionInfo, Vec<InstallOperation>)> {
|
||||
) -> Result<(PartitionInfo, Vec<InstallOperation>, Option<u64>)> {
|
||||
const CHUNK_SIZE: u64 = 2 * 1024 * 1024;
|
||||
const CHUNK_GROUP: u64 = 32;
|
||||
|
||||
@@ -921,6 +938,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 cow_estimate = 0;
|
||||
let mut operations = vec![];
|
||||
|
||||
// Read the file one group at a time. This allows for some parallelization
|
||||
@@ -957,8 +975,13 @@ pub fn compress_image(
|
||||
let mut compressed_data_group = uncompressed_data_group
|
||||
.into_par_iter()
|
||||
.map(
|
||||
|(raw_offset, raw_data)| -> Result<(Vec<u8>, InstallOperation)> {
|
||||
|(raw_offset, raw_data)| -> Result<(Vec<u8>, InstallOperation, u64)> {
|
||||
let (data, digest_compressed) = compress_chunk(&raw_data, cancel_signal)?;
|
||||
let cow_size = if need_cow_estimate {
|
||||
compress_cow_size(&raw_data, block_size)
|
||||
} else {
|
||||
0
|
||||
};
|
||||
|
||||
let extent = Extent {
|
||||
start_block: Some(raw_offset / u64::from(block_size)),
|
||||
@@ -971,19 +994,20 @@ pub fn compress_image(
|
||||
operation.dst_extents.push(extent);
|
||||
operation.data_sha256_hash = Some(digest_compressed.as_ref().to_vec());
|
||||
|
||||
Ok((data, operation))
|
||||
Ok((data, operation, cow_size))
|
||||
},
|
||||
)
|
||||
.collect::<Result<Vec<_>>>()?;
|
||||
|
||||
for (data, operation) in &mut compressed_data_group {
|
||||
for (data, operation, cow_size) in &mut compressed_data_group {
|
||||
operation.data_offset = Some(bytes_compressed);
|
||||
bytes_compressed += data.len() as u64;
|
||||
cow_estimate += *cow_size;
|
||||
}
|
||||
|
||||
let group_operations = compressed_data_group
|
||||
.into_par_iter()
|
||||
.map(|(data, operation)| -> Result<InstallOperation> {
|
||||
.map(|(data, operation, _)| -> Result<InstallOperation> {
|
||||
let mut writer = output.reopen_boxed()?;
|
||||
writer.seek(SeekFrom::Start(operation.data_offset.unwrap()))?;
|
||||
writer.write_all(&data)?;
|
||||
@@ -1001,7 +1025,16 @@ pub fn compress_image(
|
||||
hash: Some(digest_uncompressed.as_ref().to_vec()),
|
||||
};
|
||||
|
||||
Ok((partition_info, operations))
|
||||
let cow_estimate = if need_cow_estimate {
|
||||
// Because lz4_flex compresses better than official lz4.
|
||||
let fudge = cow_estimate / 100;
|
||||
|
||||
Some(cow_estimate + fudge)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
Ok((partition_info, operations, cow_estimate))
|
||||
}
|
||||
|
||||
fn extents_sorted(operations: &[InstallOperation]) -> bool {
|
||||
|
||||
@@ -23,14 +23,14 @@ use liblzma::{
|
||||
use rayon::iter::{IntoParallelRefIterator, IntoParallelRefMutIterator, ParallelIterator};
|
||||
use regex::bytes::Regex;
|
||||
use ring::digest::Context;
|
||||
use rsa::{RsaPrivateKey, RsaPublicKey};
|
||||
use rsa::RsaPublicKey;
|
||||
use thiserror::Error;
|
||||
use tracing::{debug, debug_span, trace, warn, Span};
|
||||
use x509_cert::Certificate;
|
||||
use zip::{result::ZipError, ZipArchive};
|
||||
|
||||
use crate::{
|
||||
crypto,
|
||||
crypto::{self, RsaSigningKey},
|
||||
format::{
|
||||
avb::{self, AppendedDescriptorMut, Footer, Header},
|
||||
bootimage::{self, BootImage, BootImageExt, RamdiskMeta},
|
||||
@@ -1122,7 +1122,7 @@ pub fn patch_boot_images<'a>(
|
||||
names: &[&'a str],
|
||||
open_input: impl Fn(&str) -> io::Result<Box<dyn ReadSeek>> + Sync,
|
||||
open_output: impl Fn(&str) -> io::Result<Box<dyn WriteSeek>> + Sync,
|
||||
key: &RsaPrivateKey,
|
||||
key: &RsaSigningKey,
|
||||
patchers: &[Box<dyn BootImagePatch + Sync>],
|
||||
cancel_signal: &AtomicBool,
|
||||
) -> Result<HashSet<&'a str>> {
|
||||
|
||||
@@ -11,13 +11,13 @@ use std::{
|
||||
|
||||
use memchr::memmem;
|
||||
use rayon::iter::{IntoParallelIterator, ParallelIterator};
|
||||
use rsa::RsaPrivateKey;
|
||||
use thiserror::Error;
|
||||
use tracing::{debug, debug_span, trace, Span};
|
||||
use x509_cert::Certificate;
|
||||
use zip::ZipArchive;
|
||||
|
||||
use crate::{
|
||||
crypto::RsaSigningKey,
|
||||
format::{
|
||||
avb::{self, AppendedDescriptorMut, Footer},
|
||||
ota,
|
||||
@@ -111,7 +111,7 @@ pub fn patch_system_image(
|
||||
input: &(dyn ReadSeekReopen + Sync),
|
||||
output: &(dyn WriteSeekReopen + Sync),
|
||||
certificate: &Certificate,
|
||||
key: &RsaPrivateKey,
|
||||
key: &RsaSigningKey,
|
||||
cancel_signal: &AtomicBool,
|
||||
) -> Result<(Vec<Range<u64>>, Vec<Range<u64>>)> {
|
||||
// This must be a multiple of normal filesystem block sizes (eg. 4 KiB).
|
||||
|
||||
@@ -14,6 +14,7 @@ use rsa::RsaPrivateKey;
|
||||
|
||||
use avbroot::{
|
||||
self,
|
||||
crypto::RsaSigningKey,
|
||||
format::avb::{
|
||||
self, AlgorithmType, AppendedDescriptorMut, AppendedDescriptorRef,
|
||||
ChainPartitionDescriptor, Descriptor, Footer, HashDescriptor, HashTreeDescriptor, Header,
|
||||
@@ -22,7 +23,7 @@ use avbroot::{
|
||||
stream::SharedCursor,
|
||||
};
|
||||
|
||||
fn get_test_key() -> RsaPrivateKey {
|
||||
fn get_test_key() -> RsaSigningKey {
|
||||
let data = include_str!(concat!(
|
||||
env!("CARGO_WORKSPACE_DIR"),
|
||||
"/e2e/keys/TEST_KEY_DO_NOT_USE_avb.key",
|
||||
@@ -32,7 +33,8 @@ fn get_test_key() -> RsaPrivateKey {
|
||||
"/e2e/keys/TEST_KEY_DO_NOT_USE_avb.passphrase",
|
||||
));
|
||||
|
||||
RsaPrivateKey::from_pkcs8_encrypted_pem(data, passphrase.trim_end()).unwrap()
|
||||
let key = RsaPrivateKey::from_pkcs8_encrypted_pem(data, passphrase.trim_end()).unwrap();
|
||||
RsaSigningKey::Internal(key)
|
||||
}
|
||||
|
||||
fn repeat_str(s: &str, max_len: usize) -> String {
|
||||
|
||||
@@ -7,6 +7,7 @@ use std::io::Cursor;
|
||||
|
||||
use avbroot::{
|
||||
self,
|
||||
crypto::RsaSigningKey,
|
||||
format::{
|
||||
avb::{AlgorithmType, Descriptor, HashDescriptor, Header},
|
||||
bootimage::{
|
||||
@@ -19,7 +20,7 @@ use avbroot::{
|
||||
use pkcs8::DecodePrivateKey;
|
||||
use rsa::RsaPrivateKey;
|
||||
|
||||
fn get_test_key() -> RsaPrivateKey {
|
||||
fn get_test_key() -> RsaSigningKey {
|
||||
let data = include_str!(concat!(
|
||||
env!("CARGO_WORKSPACE_DIR"),
|
||||
"/e2e/keys/TEST_KEY_DO_NOT_USE_avb.key",
|
||||
@@ -29,7 +30,8 @@ fn get_test_key() -> RsaPrivateKey {
|
||||
"/e2e/keys/TEST_KEY_DO_NOT_USE_avb.passphrase",
|
||||
));
|
||||
|
||||
RsaPrivateKey::from_pkcs8_encrypted_pem(data, passphrase.trim_end()).unwrap()
|
||||
let key = RsaPrivateKey::from_pkcs8_encrypted_pem(data, passphrase.trim_end()).unwrap();
|
||||
RsaSigningKey::Internal(key)
|
||||
}
|
||||
|
||||
fn repeat(s: &str, max_len: usize) -> String {
|
||||
|
||||
+1
-1
@@ -15,7 +15,7 @@ clap = { version = "4.4.1", features = ["derive"] }
|
||||
ctrlc = "3.4.0"
|
||||
hex = { version = "0.4.3", features = ["serde"] }
|
||||
ring = "0.17.0"
|
||||
rsa = "0.9.6"
|
||||
rsa = { version = "0.9.6", features = ["hazmat"] }
|
||||
serde = { version = "1.0.188", features = ["derive"] }
|
||||
tempfile = "3.8.0"
|
||||
toml_edit = { version = "0.22.9", features = ["serde"] }
|
||||
|
||||
+8
-8
@@ -46,8 +46,8 @@ data.version = "vendor_v4"
|
||||
data.ramdisks = [["otacerts", "first_stage", "dsu_key_dir"]]
|
||||
|
||||
[profile.pixel_v4_gki.hashes]
|
||||
original = "24a0a62cc08b96563f4872aee2fdd4a84d1a977b55c326dd9d4ddd92a1d326ea"
|
||||
patched = "29889670efea78bace221742b19e8ace88b67137fc2f46dcd9dbdf67e4e42267"
|
||||
original = "6b140c378d21eae2fa4fc581bce13a689b21bd32f5fba865698d1fd322f2f8c6"
|
||||
patched = "f00e9745f90754be28ce8355501d876759cd8336451e4c3633908fbb4217b422"
|
||||
|
||||
# Google Pixel 6a
|
||||
# What's unique: boot (boot v4, no ramdisk) + vendor_boot (vendor v4, 2 ramdisks)
|
||||
@@ -80,8 +80,8 @@ data.version = "vendor_v4"
|
||||
data.ramdisks = [["init", "otacerts", "first_stage", "dsu_key_dir"], ["dlkm"]]
|
||||
|
||||
[profile.pixel_v4_non_gki.hashes]
|
||||
original = "0e7d0924a68d46e00abe96abfa0e5f3a98d5d15a32bef7401b91fca9a19748e8"
|
||||
patched = "1a2f53d9ac3a1da75e5e7502110bda437c5a725764f16656c564d42460136279"
|
||||
original = "31963e6f81986c6686111f50e36b89e4d85ee5c02bc8e5ecd560528bc98d6fe7"
|
||||
patched = "43959409034dbb9aa0a605d7c5c0e7885012bbcd63510ec87d8e97015b37a746"
|
||||
|
||||
# Google Pixel 4a 5G
|
||||
# What's unique: boot (boot v3) + vendor_boot (vendor v3)
|
||||
@@ -115,8 +115,8 @@ data.version = "vendor_v3"
|
||||
data.ramdisks = [["otacerts", "first_stage", "dsu_key_dir"]]
|
||||
|
||||
[profile.pixel_v3.hashes]
|
||||
original = "533e6f233cb98c98c945044c2ee81a6069e66baee6f7dbcfbf7523795a11215e"
|
||||
patched = "1eeae9dba0302c2d469bd03c8bccdc0469c171204dbd676a20cb6620d2d11c5c"
|
||||
original = "e684aacb54464098c1b8e3f499efe35dff10ea792e89d71a83404620d0108b3e"
|
||||
patched = "08e03ec327bf5bd841b91ad8d53028c3439a1722b423aaaac6cc0ceee4ef66b1"
|
||||
|
||||
# Google Pixel 4a
|
||||
# What's unique: boot (boot v2)
|
||||
@@ -144,5 +144,5 @@ data.type = "vbmeta"
|
||||
data.deps = ["system"]
|
||||
|
||||
[profile.pixel_v2.hashes]
|
||||
original = "958dfa428abd2901178b90147903d7857ed78d6c016f6cb3af30d024a22a8f9a"
|
||||
patched = "b0d18ef350ca7b6499b7de4b0182f4ac3be7288ad238ce888708643e750f7631"
|
||||
original = "ee9568797d9195985f14753b89949d8ebb08c8863a32eceeeec6e8d94661b1cf"
|
||||
patched = "5e265094d4164cedde8f483911c58860f6008b314dc8e5ed3b44deb53fbb2f96"
|
||||
|
||||
+26
-2
@@ -3,10 +3,10 @@
|
||||
* SPDX-License-Identifier: GPL-3.0-only
|
||||
*/
|
||||
|
||||
use std::path::PathBuf;
|
||||
use std::{ffi::OsString, path::PathBuf};
|
||||
|
||||
use avbroot::cli::args::{LogFormat, LogLevel};
|
||||
use clap::{Args, Parser, Subcommand};
|
||||
use clap::{Args, Parser, Subcommand, ValueEnum};
|
||||
|
||||
#[derive(Debug, Args)]
|
||||
pub struct ProfileGroup {
|
||||
@@ -76,3 +76,27 @@ pub struct Cli {
|
||||
#[arg(long, global = true, value_name = "FORMAT", default_value_t = LogFormat::Medium)]
|
||||
pub log_format: LogFormat,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, ValueEnum)]
|
||||
pub enum PassSource {
|
||||
Env,
|
||||
File,
|
||||
}
|
||||
|
||||
#[derive(Debug, Parser)]
|
||||
pub struct HelperCli {
|
||||
/// Signature algorithm.
|
||||
pub algorithm: String,
|
||||
|
||||
/// Public key.
|
||||
#[arg(value_name = "FILE", value_parser)]
|
||||
pub public_key: PathBuf,
|
||||
|
||||
/// Non-interactive password source.
|
||||
#[arg(value_name = "SOURCE")]
|
||||
pub pass_source: PassSource,
|
||||
|
||||
/// Non-interactive password source value.
|
||||
#[arg(value_name = "VALUE", value_parser)]
|
||||
pub pass_source_value: OsString,
|
||||
}
|
||||
|
||||
+184
-53
@@ -9,10 +9,11 @@ mod config;
|
||||
|
||||
use std::{
|
||||
collections::{BTreeMap, BTreeSet},
|
||||
ffi::OsStr,
|
||||
env,
|
||||
ffi::{OsStr, OsString},
|
||||
fs::{self, File},
|
||||
io::{self, BufReader, BufWriter, Cursor, Seek, SeekFrom, Write},
|
||||
path::Path,
|
||||
io::{self, BufReader, BufWriter, Cursor, Read, Seek, SeekFrom, Write},
|
||||
path::{Path, PathBuf},
|
||||
sync::{
|
||||
atomic::{AtomicBool, Ordering},
|
||||
Arc,
|
||||
@@ -22,7 +23,7 @@ use std::{
|
||||
use anyhow::{anyhow, bail, Context, Result};
|
||||
use avbroot::{
|
||||
cli::ota::{ExtractCli, PatchCli, VerifyCli},
|
||||
crypto::{self, PassphraseSource},
|
||||
crypto::{self, PassphraseSource, RsaSigningKey},
|
||||
format::{
|
||||
avb::{
|
||||
self, AlgorithmType, ChainPartitionDescriptor, Descriptor, Footer, HashDescriptor,
|
||||
@@ -48,15 +49,15 @@ use avbroot::{
|
||||
stream::{self, CountingWriter, HashingReader, PSeekFile, Reopen, ToWriter},
|
||||
};
|
||||
use clap::Parser;
|
||||
use rsa::RsaPrivateKey;
|
||||
use tempfile::{NamedTempFile, TempDir};
|
||||
use rsa::{rand_core::OsRng, traits::PublicKeyParts, BigUint};
|
||||
use tempfile::TempDir;
|
||||
use topological_sort::TopologicalSort;
|
||||
use tracing::{info, info_span};
|
||||
use x509_cert::Certificate;
|
||||
use zip::{write::FileOptions, CompressionMethod, ZipWriter};
|
||||
|
||||
use crate::{
|
||||
cli::{Cli, Command, ListCli, ProfileGroup, TestCli},
|
||||
cli::{Cli, Command, HelperCli, ListCli, PassSource, ProfileGroup, TestCli},
|
||||
config::{
|
||||
Avb, BootData, BootVersion, Config, Data, DmVerityContent, DmVerityData, OtaInfo,
|
||||
Partition, Profile, RamdiskContent, VbmetaData,
|
||||
@@ -100,7 +101,7 @@ fn append_avb(
|
||||
avb: &Avb,
|
||||
hash_tree: bool,
|
||||
ota_info: &OtaInfo,
|
||||
key_avb: &RsaPrivateKey,
|
||||
key_avb: &RsaSigningKey,
|
||||
cancel_signal: &AtomicBool,
|
||||
) -> Result<()> {
|
||||
let image_size = file.seek(SeekFrom::End(0))?;
|
||||
@@ -298,7 +299,7 @@ fn create_boot_image(
|
||||
avb: &Avb,
|
||||
boot_data: &BootData,
|
||||
ota_info: &OtaInfo,
|
||||
key_avb: &RsaPrivateKey,
|
||||
key_avb: &RsaSigningKey,
|
||||
cert_ota: &Certificate,
|
||||
cancel_signal: &AtomicBool,
|
||||
) -> Result<()> {
|
||||
@@ -425,7 +426,7 @@ fn create_dm_verity_image(
|
||||
avb: &Avb,
|
||||
dm_verity_data: &DmVerityData,
|
||||
ota_info: &OtaInfo,
|
||||
key_avb: &RsaPrivateKey,
|
||||
key_avb: &RsaSigningKey,
|
||||
cert_ota: &Certificate,
|
||||
cancel_signal: &AtomicBool,
|
||||
) -> Result<()> {
|
||||
@@ -454,7 +455,7 @@ fn create_vbmeta_image(
|
||||
avb: &Avb,
|
||||
vbmeta_data: &VbmetaData,
|
||||
inputs: &BTreeMap<String, PSeekFile>,
|
||||
key: &RsaPrivateKey,
|
||||
key: &RsaSigningKey,
|
||||
) -> Result<()> {
|
||||
let mut descriptors = Vec::new();
|
||||
|
||||
@@ -506,7 +507,7 @@ fn create_vbmeta_image(
|
||||
fn create_partition_images(
|
||||
partitions: &BTreeMap<String, Partition>,
|
||||
ota_info: &OtaInfo,
|
||||
key_avb: &RsaPrivateKey,
|
||||
key_avb: &RsaSigningKey,
|
||||
cert_ota: &Certificate,
|
||||
cancel_signal: &AtomicBool,
|
||||
) -> Result<BTreeMap<String, PSeekFile>> {
|
||||
@@ -576,7 +577,7 @@ fn create_payload(
|
||||
partitions: &BTreeMap<String, Partition>,
|
||||
inputs: &BTreeMap<String, PSeekFile>,
|
||||
ota_info: &OtaInfo,
|
||||
key_ota: &RsaPrivateKey,
|
||||
key_ota: &RsaSigningKey,
|
||||
cancel_signal: &AtomicBool,
|
||||
) -> Result<(String, u64)> {
|
||||
let dynamic_partitions_names = partitions
|
||||
@@ -593,8 +594,14 @@ fn create_payload(
|
||||
.map(PSeekFile::new)
|
||||
.with_context(|| format!("Failed to create temp file for: {name}"))?;
|
||||
|
||||
let (partition_info, operations) =
|
||||
payload::compress_image(file, &writer, name, 4096, cancel_signal)?;
|
||||
let (partition_info, operations, cow_estimate) = payload::compress_image(
|
||||
file,
|
||||
&writer,
|
||||
name,
|
||||
4096,
|
||||
dynamic_partitions_names.contains(name),
|
||||
cancel_signal,
|
||||
)?;
|
||||
|
||||
compressed.insert(name, writer);
|
||||
|
||||
@@ -617,7 +624,7 @@ fn create_payload(
|
||||
fec_roots: None,
|
||||
version: None,
|
||||
merge_operations: vec![],
|
||||
estimate_cow_size: None,
|
||||
estimate_cow_size: cow_estimate,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -638,7 +645,7 @@ fn create_payload(
|
||||
}],
|
||||
snapshot_enabled: Some(true),
|
||||
vabc_enabled: Some(true),
|
||||
vabc_compression_param: Some("gz".to_owned()),
|
||||
vabc_compression_param: Some("lz4".to_owned()),
|
||||
cow_version: Some(2),
|
||||
vabc_feature_set: None,
|
||||
}),
|
||||
@@ -692,8 +699,8 @@ fn create_ota(
|
||||
output: &Path,
|
||||
ota_info: &OtaInfo,
|
||||
profile: &Profile,
|
||||
key_avb: &RsaPrivateKey,
|
||||
key_ota: &RsaPrivateKey,
|
||||
key_avb: &RsaSigningKey,
|
||||
key_ota: &RsaSigningKey,
|
||||
cert_ota: &Certificate,
|
||||
cancel_signal: &AtomicBool,
|
||||
) -> Result<()> {
|
||||
@@ -844,15 +851,18 @@ fn create_fake_magisk(output: &Path) -> Result<()> {
|
||||
}
|
||||
|
||||
struct KeySet {
|
||||
avb_key: RsaPrivateKey,
|
||||
ota_key: RsaPrivateKey,
|
||||
avb_key: RsaSigningKey,
|
||||
ota_key: RsaSigningKey,
|
||||
ota_cert: Certificate,
|
||||
avb_key_file: NamedTempFile,
|
||||
avb_pass_file: NamedTempFile,
|
||||
avb_pkmd_file: NamedTempFile,
|
||||
ota_key_file: NamedTempFile,
|
||||
ota_pass_file: NamedTempFile,
|
||||
ota_cert_file: NamedTempFile,
|
||||
_key_dir: TempDir,
|
||||
avb_key_file: PathBuf,
|
||||
avb_public_key_file: PathBuf,
|
||||
avb_pass_file: PathBuf,
|
||||
avb_pkmd_file: PathBuf,
|
||||
ota_key_file: PathBuf,
|
||||
ota_public_key_file: PathBuf,
|
||||
ota_pass_file: PathBuf,
|
||||
ota_cert_file: PathBuf,
|
||||
}
|
||||
|
||||
macro_rules! new_keys_with_prefix {
|
||||
@@ -901,13 +911,8 @@ macro_rules! new_keys_with_prefix {
|
||||
}
|
||||
|
||||
impl KeySet {
|
||||
fn write_temp(data: &[u8]) -> Result<NamedTempFile> {
|
||||
let mut temp_file =
|
||||
NamedTempFile::new().context("Failed to create temp file for test keys")?;
|
||||
temp_file
|
||||
.write_all(data)
|
||||
.with_context(|| format!("Failed to write test key data: {:?}", temp_file.path()))?;
|
||||
Ok(temp_file)
|
||||
fn write(path: &Path, data: &[u8]) -> Result<()> {
|
||||
fs::write(path, data).with_context(|| format!("Failed to write test key data: {path:?}"))
|
||||
}
|
||||
|
||||
fn new_with_data(
|
||||
@@ -918,36 +923,54 @@ impl KeySet {
|
||||
ota_pass: &[u8],
|
||||
ota_cert: &[u8],
|
||||
) -> Result<Self> {
|
||||
let avb_key_file = Self::write_temp(avb_key)?;
|
||||
let avb_pass_file = Self::write_temp(avb_pass)?;
|
||||
let avb_pkmd_file = Self::write_temp(avb_pkmd)?;
|
||||
let ota_key_file = Self::write_temp(ota_key)?;
|
||||
let ota_pass_file = Self::write_temp(ota_pass)?;
|
||||
let ota_cert_file = Self::write_temp(ota_cert)?;
|
||||
let key_dir = TempDir::new().context("Failed to create temp directory")?;
|
||||
let avb_key_file = key_dir.path().join("avb.key");
|
||||
let avb_public_key_file = key_dir.path().join("avb.public.key");
|
||||
let avb_pass_file = key_dir.path().join("avb.passphrase");
|
||||
let avb_pkmd_file = key_dir.path().join("avb_pkmd.bin");
|
||||
let ota_key_file = key_dir.path().join("ota.key");
|
||||
let ota_public_key_file = key_dir.path().join("ota.public.key");
|
||||
let ota_pass_file = key_dir.path().join("ota.passphrase");
|
||||
let ota_cert_file = key_dir.path().join("ota.crt");
|
||||
|
||||
Self::write(&avb_key_file, avb_key)?;
|
||||
Self::write(&avb_pass_file, avb_pass)?;
|
||||
Self::write(&avb_pkmd_file, avb_pkmd)?;
|
||||
Self::write(&ota_key_file, ota_key)?;
|
||||
Self::write(&ota_pass_file, ota_pass)?;
|
||||
Self::write(&ota_cert_file, ota_cert)?;
|
||||
|
||||
let avb_key = crypto::read_pem_key_file(
|
||||
avb_key_file.path(),
|
||||
&PassphraseSource::File(avb_pass_file.path().to_owned()),
|
||||
&avb_key_file,
|
||||
&PassphraseSource::File(avb_pass_file.clone()),
|
||||
)
|
||||
.map(RsaSigningKey::Internal)
|
||||
.context("Failed to load AVB test key")?;
|
||||
|
||||
let ota_key = crypto::read_pem_key_file(
|
||||
ota_key_file.path(),
|
||||
&PassphraseSource::File(ota_pass_file.path().to_owned()),
|
||||
&ota_key_file,
|
||||
&PassphraseSource::File(ota_pass_file.clone()),
|
||||
)
|
||||
.map(RsaSigningKey::Internal)
|
||||
.context("Failed to load OTA test key")?;
|
||||
|
||||
let ota_cert = crypto::read_pem_cert_file(ota_cert_file.path())
|
||||
.context("Failed to load OTA test cert")?;
|
||||
crypto::write_pem_public_key_file(&avb_public_key_file, &avb_key.to_public_key())?;
|
||||
crypto::write_pem_public_key_file(&ota_public_key_file, &ota_key.to_public_key())?;
|
||||
|
||||
let ota_cert =
|
||||
crypto::read_pem_cert_file(&ota_cert_file).context("Failed to load OTA test cert")?;
|
||||
|
||||
Ok(Self {
|
||||
avb_key,
|
||||
ota_key,
|
||||
ota_cert,
|
||||
_key_dir: key_dir,
|
||||
avb_key_file,
|
||||
avb_public_key_file,
|
||||
avb_pass_file,
|
||||
avb_pkmd_file,
|
||||
ota_key_file,
|
||||
ota_public_key_file,
|
||||
ota_pass_file,
|
||||
ota_cert_file,
|
||||
})
|
||||
@@ -960,12 +983,20 @@ impl KeySet {
|
||||
fn patch_image(
|
||||
input_file: &Path,
|
||||
output_file: &Path,
|
||||
system_image_file: &Path,
|
||||
extra_args: &[&OsStr],
|
||||
keys: &KeySet,
|
||||
signing_helper: bool,
|
||||
cancel_signal: &AtomicBool,
|
||||
) -> Result<()> {
|
||||
info!("Patching OTA: {input_file:?} -> {output_file:?}");
|
||||
|
||||
let (avb_key_file, ota_key_file) = if signing_helper {
|
||||
(&keys.avb_public_key_file, &keys.ota_public_key_file)
|
||||
} else {
|
||||
(&keys.avb_key_file, &keys.ota_key_file)
|
||||
};
|
||||
|
||||
// We're intentionally using the CLI interface.
|
||||
let mut args: Vec<&OsStr> = vec![
|
||||
OsStr::new("patch"),
|
||||
@@ -973,18 +1004,29 @@ fn patch_image(
|
||||
input_file.as_os_str(),
|
||||
OsStr::new("--output"),
|
||||
output_file.as_os_str(),
|
||||
OsStr::new("--replace"),
|
||||
OsStr::new("system"),
|
||||
system_image_file.as_os_str(),
|
||||
OsStr::new("--key-avb"),
|
||||
keys.avb_key_file.path().as_os_str(),
|
||||
avb_key_file.as_os_str(),
|
||||
OsStr::new("--pass-avb-file"),
|
||||
keys.avb_pass_file.path().as_os_str(),
|
||||
keys.avb_pass_file.as_os_str(),
|
||||
OsStr::new("--key-ota"),
|
||||
keys.ota_key_file.path().as_os_str(),
|
||||
ota_key_file.as_os_str(),
|
||||
OsStr::new("--pass-ota-file"),
|
||||
keys.ota_pass_file.path().as_os_str(),
|
||||
keys.ota_pass_file.as_os_str(),
|
||||
OsStr::new("--cert-ota"),
|
||||
keys.ota_cert_file.path().as_os_str(),
|
||||
keys.ota_cert_file.as_os_str(),
|
||||
OsStr::new("--dsu"),
|
||||
];
|
||||
|
||||
let argv0: OsString;
|
||||
if signing_helper {
|
||||
argv0 = env::args_os().next().unwrap();
|
||||
args.push(OsStr::new("--signing-helper"));
|
||||
args.push(&argv0);
|
||||
}
|
||||
|
||||
args.extend_from_slice(extra_args);
|
||||
|
||||
let cli = PatchCli::try_parse_from(args)?;
|
||||
@@ -1016,9 +1058,9 @@ fn verify_image(input_file: &Path, keys: &KeySet, cancel_signal: &AtomicBool) ->
|
||||
OsStr::new("--input"),
|
||||
input_file.as_os_str(),
|
||||
OsStr::new("--public-key-avb"),
|
||||
keys.avb_pkmd_file.path().as_os_str(),
|
||||
keys.avb_pkmd_file.as_os_str(),
|
||||
OsStr::new("--cert-ota"),
|
||||
keys.ota_cert_file.path().as_os_str(),
|
||||
keys.ota_cert_file.as_os_str(),
|
||||
])?;
|
||||
avbroot::cli::ota::verify_subcommand(&cli, cancel_signal)?;
|
||||
|
||||
@@ -1119,11 +1161,18 @@ fn test_subcommand(cli: &TestCli, cancel_signal: &AtomicBool) -> Result<()> {
|
||||
.with_context(|| format!("[{name}] Failed to verify original OTA hash"))?;
|
||||
|
||||
// Patch once using Magisk.
|
||||
extract_image(&out_original, &profile_dir, cancel_signal)
|
||||
.with_context(|| format!("[{name}] Failed to extract OTA"))?;
|
||||
|
||||
let system_image = profile_dir.join("system.img");
|
||||
|
||||
patch_image(
|
||||
&out_original,
|
||||
&out_magisk,
|
||||
&system_image,
|
||||
&args_magisk,
|
||||
&test_keys,
|
||||
false,
|
||||
cancel_signal,
|
||||
)
|
||||
.with_context(|| format!("[{name}] Failed to patch OTA"))?;
|
||||
@@ -1149,8 +1198,10 @@ fn test_subcommand(cli: &TestCli, cancel_signal: &AtomicBool) -> Result<()> {
|
||||
patch_image(
|
||||
&out_original,
|
||||
&out_prepatched,
|
||||
&system_image,
|
||||
&args_prepatched,
|
||||
&test_keys,
|
||||
true,
|
||||
cancel_signal,
|
||||
)
|
||||
.with_context(|| format!("[{name}] Failed to patch OTA"))?;
|
||||
@@ -1175,7 +1226,87 @@ fn list_subcommand(cli: &ListCli) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// A basic --signing-helper implementation that just signs via RustCrypto.
|
||||
fn helper_mode() -> Result<()> {
|
||||
let cli = HelperCli::parse();
|
||||
|
||||
let private_key_path = {
|
||||
let parent = cli.public_key.parent().unwrap_or(Path::new("."));
|
||||
let name = cli
|
||||
.public_key
|
||||
.file_name()
|
||||
.and_then(|n| n.to_str())
|
||||
.ok_or_else(|| anyhow!("Bad filename: {:?}", cli.public_key))?;
|
||||
|
||||
parent.join(name.replace(".public", ""))
|
||||
};
|
||||
let source = match cli.pass_source {
|
||||
PassSource::Env => PassphraseSource::EnvVar(cli.pass_source_value),
|
||||
PassSource::File => PassphraseSource::File(cli.pass_source_value.into()),
|
||||
};
|
||||
let private_key = crypto::read_pem_key_file(&private_key_path, &source)
|
||||
.with_context(|| format!("Failed to load private key: {private_key_path:?}"))?;
|
||||
let public_key = crypto::read_pem_public_key_file(&cli.public_key)
|
||||
.with_context(|| format!("Failed to load public key: {:?}", cli.public_key))?;
|
||||
|
||||
if private_key.to_public_key() != public_key {
|
||||
bail!("Private key does not match public key");
|
||||
}
|
||||
|
||||
let (hash_algo, key_algo) = cli
|
||||
.algorithm
|
||||
.split_once('_')
|
||||
.ok_or_else(|| anyhow!("Unknown algorithm: {:?}", cli.algorithm))?;
|
||||
|
||||
if key_algo != format!("RSA{}", private_key.size() * 8) {
|
||||
bail!(
|
||||
"{key_algo} does not match key size ({})",
|
||||
private_key.size() * 8
|
||||
);
|
||||
} else if hash_algo != "SHA256" && hash_algo != "SHA512" {
|
||||
bail!("Unknown hash algorithm: {hash_algo}");
|
||||
}
|
||||
|
||||
let mut padded_digest = vec![];
|
||||
io::stdin()
|
||||
.read_to_end(&mut padded_digest)
|
||||
.context("Failed to read padded digest from stdin")?;
|
||||
|
||||
if padded_digest.len() != private_key.size() {
|
||||
bail!(
|
||||
"Padded digest size ({}) bytes does not match key size ({})",
|
||||
padded_digest.len(),
|
||||
private_key.size()
|
||||
);
|
||||
}
|
||||
|
||||
// The input is already padded, so perform a raw RSA signing operation.
|
||||
let mut signature = rsa::hazmat::rsa_decrypt_and_check(
|
||||
&private_key,
|
||||
None::<&mut OsRng>,
|
||||
&BigUint::from_bytes_be(&padded_digest),
|
||||
)
|
||||
.context("Failed to sign digest")?
|
||||
.to_bytes_le();
|
||||
signature.resize(private_key.size(), 0);
|
||||
signature.reverse();
|
||||
|
||||
io::stdout()
|
||||
.write_all(&signature)
|
||||
.context("Failed to write signature to stdout")?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn main() -> Result<()> {
|
||||
const ENV_HELPER_MODE: &str = "E2E_HELPER_MODE";
|
||||
|
||||
// Re-invoking ourselves will execute as the helper script instead.
|
||||
if env::var_os(ENV_HELPER_MODE).is_some() {
|
||||
return helper_mode();
|
||||
}
|
||||
env::set_var(ENV_HELPER_MODE, "true");
|
||||
|
||||
// Set up a cancel signal so we can properly clean up any temporary files.
|
||||
let cancel_signal = Arc::new(AtomicBool::new(false));
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user