From e059107b732b79265173dc887e4b97f3101f40de Mon Sep 17 00:00:00 2001 From: Ali Zhang Date: Thu, 30 Sep 2021 17:18:27 -0700 Subject: [PATCH] pw_software_update: Add root metadata to a bundle Adds support for including a root metadata when creating a bundle. No-Docs-Update-Reason: module in early development. Change-Id: Ibdced04fd355e8f520d0d3ba6e5cf25276929d72 Reviewed-on: https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/63640 Commit-Queue: Ali Zhang Reviewed-by: Joe Ethier --- .../py/pw_software_update/update_bundle.py | 41 +++++++++++++------ pw_software_update/py/update_bundle_test.py | 11 +++-- pw_software_update/update_bundle.proto | 26 +++++++++--- 3 files changed, 58 insertions(+), 20 deletions(-) diff --git a/pw_software_update/py/pw_software_update/update_bundle.py b/pw_software_update/py/pw_software_update/update_bundle.py index eef804e73..e45234725 100644 --- a/pw_software_update/py/pw_software_update/update_bundle.py +++ b/pw_software_update/py/pw_software_update/update_bundle.py @@ -21,7 +21,7 @@ import shutil from typing import Dict, Iterable, Optional, Tuple from pw_software_update import metadata -from pw_software_update.tuf_pb2 import SignedTargetsMetadata +from pw_software_update.tuf_pb2 import SignedRootMetadata, SignedTargetsMetadata from pw_software_update.update_bundle_pb2 import UpdateBundle _LOG = logging.getLogger(__package__) @@ -76,16 +76,17 @@ def targets_from_directory( def gen_unsigned_update_bundle( - targets: Dict[Path, str], - persist: Optional[Path] = None, - targets_metadata_version: int = metadata.DEFAULT_METADATA_VERSION -) -> UpdateBundle: + targets: Dict[Path, str], + persist: Optional[Path] = None, + targets_metadata_version: int = metadata.DEFAULT_METADATA_VERSION, + root_metadata: SignedRootMetadata = None) -> UpdateBundle: """Given a set of targets, generates an unsigned UpdateBundle. Args: targets: A dict mapping payload Paths to their target names. persist: If not None, persist the raw TUF repository to this directory. targets_metadata_version: version number for the targets metadata. + root_metadata: Optional signed Root metadata. The input targets will be treated as an ephemeral TUF repository for the purposes of building an UpdateBundle instance. This approach differs @@ -97,6 +98,10 @@ def gen_unsigned_update_bundle( NOTE: If path separator characters (like '/') are used in target names, then persisting the repository to disk via the 'persist' argument will create the corresponding directory structure. + + NOTE: If a root metadata is included, the client is expected to first + upgrade its on-device trusted root metadata before verifying the rest of + the bundle. """ if persist: if persist.exists() and not persist.is_dir(): @@ -119,7 +124,9 @@ def gen_unsigned_update_bundle( target_payloads, version=targets_metadata_version) unsigned_targets_metadata = SignedTargetsMetadata( serialized_targets_metadata=targets_metadata.SerializeToString()) + return UpdateBundle( + root_metadata=root_metadata, targets_metadata=dict(targets=unsigned_targets_metadata), target_payloads=target_payloads) @@ -164,23 +171,33 @@ def parse_args() -> argparse.Namespace: type=int, default=metadata.DEFAULT_METADATA_VERSION, help='Version number for the targets metadata') + parser.add_argument('--signed-root-metadata', + type=Path, + default=None, + help='Path to the signed Root metadata') return parser.parse_args() -def main( - targets: Iterable[str], - out: Path, - persist: Path = None, - targets_metadata_version: int = metadata.DEFAULT_METADATA_VERSION -) -> None: +def main(targets: Iterable[str], + out: Path, + persist: Path = None, + targets_metadata_version: int = metadata.DEFAULT_METADATA_VERSION, + signed_root_metadata: Path = None) -> None: """Generates an UpdateBundle and serializes it to disk.""" target_dict = {} for target_arg in targets: path, target_name = parse_target_arg(target_arg) target_dict[path] = target_name + root_metadata = None + if signed_root_metadata: + root_metadata = SignedRootMetadata.FromString( + signed_root_metadata.read_bytes()) + bundle = gen_unsigned_update_bundle(target_dict, persist, - targets_metadata_version) + targets_metadata_version, + root_metadata) + out.write_bytes(bundle.SerializeToString()) diff --git a/pw_software_update/py/update_bundle_test.py b/pw_software_update/py/update_bundle_test.py index 444dd752a..a4279f6f9 100644 --- a/pw_software_update/py/update_bundle_test.py +++ b/pw_software_update/py/update_bundle_test.py @@ -18,7 +18,7 @@ import tempfile import unittest from pw_software_update import update_bundle -from pw_software_update.tuf_pb2 import TargetsMetadata +from pw_software_update.tuf_pb2 import SignedRootMetadata, TargetsMetadata class TargetsFromDirectoryTest(unittest.TestCase): @@ -128,18 +128,23 @@ class GenUnsignedUpdateBundleTest(unittest.TestCase): baz_path: 'baz', qux_path: 'qux', } + serialized_root_metadata_bytes = b'\x12\x34\x56\x78' bundle = update_bundle.gen_unsigned_update_bundle( - targets, targets_metadata_version=42) + targets, + targets_metadata_version=42, + root_metadata=SignedRootMetadata( + serialized_root_metadata=serialized_root_metadata_bytes)) self.assertEqual(foo_bytes, bundle.target_payloads['foo']) self.assertEqual(bar_bytes, bundle.target_payloads['bar']) self.assertEqual(baz_bytes, bundle.target_payloads['baz']) self.assertEqual(qux_bytes, bundle.target_payloads['qux']) - targets_metadata = TargetsMetadata.FromString( bundle.targets_metadata['targets'].serialized_targets_metadata) self.assertEqual(targets_metadata.common_metadata.version, 42) + self.assertEqual(serialized_root_metadata_bytes, + bundle.root_metadata.serialized_root_metadata) def test_persist_to_disk(self): """Tests persisting the TUF repo to disk for debugging""" diff --git a/pw_software_update/update_bundle.proto b/pw_software_update/update_bundle.proto index f5f17576c..7af0f02d3 100644 --- a/pw_software_update/update_bundle.proto +++ b/pw_software_update/update_bundle.proto @@ -19,11 +19,6 @@ package pw.software_update; import "pw_software_update/tuf.proto"; message UpdateBundle { - // NOTE/TODO: SignedRootMetadata is not currently included as part of the - // UpdateBundle. Updating of the root metdata needs to be handled in a - // separate project-specific process. A standard upstream process can be added - // in the future. - // The timestamp role is used for freshness check of the snapshot. Any // project-specific update metadata should go in the top-level // targets_metadata or with the TargetFile information @@ -46,6 +41,27 @@ message UpdateBundle { // the file lives relative to the base directory of the repository, as // described in the target metadata. e.g. "path/to/amber_tools/0". map target_payloads = 4; + + // If present, a client will attempt to upgrade its on-device trusted root + // metadata to the root metadata included in the bundle, following the + // standard "Update the root role" flow specified in the TUF spec, but + // without "version climbing". + // + // The exact steps are: + // 1. Check if there is a root metadata in the bundle. + // 2. If the root metadata IS NOT included, assume on-device root metadata + // is up-to-date and continue with the rest of metadata verification. + // 3. If the root metadata IS included, verify the new root metadata using + // the on-device root metadata. + // 4. If the verification is successful, persist new root metadata and + // continue with the rest of metadata verification. Otherwise abort the + // update session. + // + // The key deviation from standard flow is the client assumes it can always + // directly upgrade to the single new root metadata in the update bundle, + // without any step-stone history root metadata. This works only because + // we are not supporting (more than 1) root key rotations. + optional SignedRootMetadata root_metadata = 5; } // Update bundle metadata