xref: /aosp_15_r20/external/pigweed/pw_software_update/py/pw_software_update/generate_test_bundle.py (revision 61c4878ac05f98d0ceed94b57d316916de578985)
1# Copyright 2021 The Pigweed Authors
2#
3# Licensed under the Apache License, Version 2.0 (the "License"); you may not
4# use this file except in compliance with the License. You may obtain a copy of
5# the License at
6#
7#     https://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
11# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
12# License for the specific language governing permissions and limitations under
13# the License.
14"""Script for generating test bundles"""
15
16import argparse
17import subprocess
18import sys
19
20from pw_software_update import dev_sign, keys, metadata, root_metadata
21from pw_software_update.update_bundle_pb2 import Manifest, UpdateBundle
22from pw_software_update.tuf_pb2 import (
23    RootMetadata,
24    SignedRootMetadata,
25    TargetsMetadata,
26    SignedTargetsMetadata,
27)
28
29from cryptography.hazmat.primitives.asymmetric import ec
30from cryptography.hazmat.primitives import serialization
31
32HEADER = """// Copyright 2021 The Pigweed Authors
33//
34// Licensed under the Apache License, Version 2.0 (the "License"); you may not
35// use this file except in compliance with the License. You may obtain a copy
36// of the License at
37//
38//     https://www.apache.org/licenses/LICENSE-2.0
39//
40// Unless required by applicable law or agreed to in writing, software
41// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
42// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
43// License for the specific language governing permissions and limitations under
44// the License.
45
46#pragma once
47
48#include "pw_bytes/span.h"
49
50"""
51
52TEST_DEV_KEY = """-----BEGIN PRIVATE KEY-----
53MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgVgMQBOTJyx1xOafy
54WTs2VkACf7Uo3RbP9Vun+oKXtMihRANCAATV7XJljxeUs2z2wqM5Q/kohAra1620
55zXT90N9a3UR+IHksTd1OA7wFq220IQB/e4eVzbcOprN0MMMuSgXMxL8p
56-----END PRIVATE KEY-----"""
57
58TEST_PROD_KEY = """-----BEGIN PRIVATE KEY-----
59MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQg73MLNmB/fPNX75Pl
60YdynPtJkM2gGOWfIcHDuwuxSQmqhRANCAARpvjrXkjG2Fp+ZgREtxeTBBmJmWGS9
618Ny2tXY+Qggzl77G7wvCNF5+koz7ecsV6sKjK+dFiAXOIdqlga7p2j0A
62-----END PRIVATE KEY-----"""
63
64TEST_TARGETS_DEV_KEY = """-----BEGIN PRIVATE KEY-----
65MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQggRCrido5vZOnkULH
66sxQDt9Qoe/TlEKoqa1bhO1HFbi6hRANCAASVwdXbGWM7+f/r+Z2W6Dbd7CQA0Cbb
67pkBv5PnA+DZnCkFhLW2kTn89zQv8W1x4m9maoINp9QPXQ4/nXlrVHqDg
68-----END PRIVATE KEY-----"""
69
70TEST_TARGETS_PROD_KEY = """-----BEGIN PRIVATE KEY-----
71MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgx2VdB2EsUKghuLMG
72RmxzqX2jnLTq5pxsFgO5Rrf5jlehRANCAASVijeDpemxVSlgZOOW0yvwE5QkXkq0
73geWonkusMP0+MXopnmN0QlpgaCnG40TSr/W+wFjRmNCklL4dXk01oCwD
74-----END PRIVATE KEY-----"""
75
76TEST_ROOT_VERSION = 2
77TEST_TARGETS_VERSION = 2
78
79USER_MANIFEST_FILE_NAME = 'user_manifest'
80
81TARGET_FILES = {
82    'file1': 'file 1 content'.encode(),
83    'file2': 'file 2 content'.encode(),
84    USER_MANIFEST_FILE_NAME: 'user manfiest content'.encode(),
85}
86
87
88def byte_array_declaration(data: bytes, name: str) -> str:
89    """Generates a byte C array declaration for a byte array"""
90    type_name = '[[maybe_unused]] const std::byte'
91    byte_str = ''.join([f'std::byte{{0x{b:02x}}},' for b in data])
92    array_body = f'{{{byte_str}}}'
93    return f'{type_name} {name}[] = {array_body};'
94
95
96def proto_array_declaration(proto, name: str) -> str:
97    """Generates a byte array declaration for a proto"""
98    return byte_array_declaration(proto.SerializeToString(), name)
99
100
101def private_key_public_pem_bytes(key: ec.EllipticCurvePrivateKey) -> bytes:
102    """Serializes the public part of a private key in PEM format"""
103    return key.public_key().public_bytes(
104        serialization.Encoding.PEM,
105        serialization.PublicFormat.SubjectPublicKeyInfo,
106    )
107
108
109def private_key_private_pem_bytes(key: ec.EllipticCurvePrivateKey) -> bytes:
110    """Serializes the private part of a private key in PEM format"""
111    return key.private_bytes(
112        encoding=serialization.Encoding.PEM,
113        format=serialization.PrivateFormat.PKCS8,
114        encryption_algorithm=serialization.NoEncryption(),
115    )
116
117
118class Bundle:
119    """A helper for test UpdateBundle generation"""
120
121    def __init__(self):
122        self._root_dev_key = serialization.load_pem_private_key(
123            TEST_DEV_KEY.encode(), None
124        )
125        self._root_prod_key = serialization.load_pem_private_key(
126            TEST_PROD_KEY.encode(), None
127        )
128        self._targets_dev_key = serialization.load_pem_private_key(
129            TEST_TARGETS_DEV_KEY.encode(), None
130        )
131        self._targets_prod_key = serialization.load_pem_private_key(
132            TEST_TARGETS_PROD_KEY.encode(), None
133        )
134        self._payloads: dict[str, bytes] = {}
135        # Adds some update files.
136        for key, value in TARGET_FILES.items():
137            self.add_payload(key, value)
138
139    def add_payload(self, name: str, payload: bytes) -> None:
140        """Adds a payload to the bundle"""
141        self._payloads[name] = payload
142
143    def generate_dev_root_metadata(self) -> RootMetadata:
144        """Generates a root metadata with the dev key"""
145        # The dev root metadata contains both the prod and the dev public key,
146        # so that it can rotate to prod. But it will only use a dev targets
147        # key.
148        return root_metadata.gen_root_metadata(
149            root_metadata.RootKeys(
150                [
151                    private_key_public_pem_bytes(self._root_dev_key),
152                    private_key_public_pem_bytes(self._root_prod_key),
153                ]
154            ),
155            root_metadata.TargetsKeys(
156                [private_key_public_pem_bytes(self._targets_dev_key)]
157            ),
158            TEST_ROOT_VERSION,
159        )
160
161    def generate_prod_root_metadata(self) -> RootMetadata:
162        """Generates a root metadata with the prod key"""
163        # The prod root metadta contains only the prod public key and uses the
164        # prod targets key
165        return root_metadata.gen_root_metadata(
166            root_metadata.RootKeys(
167                [private_key_public_pem_bytes(self._root_prod_key)]
168            ),
169            root_metadata.TargetsKeys(
170                [private_key_public_pem_bytes(self._targets_prod_key)]
171            ),
172            TEST_ROOT_VERSION,
173        )
174
175    def generate_dev_signed_root_metadata(self) -> SignedRootMetadata:
176        """Generates a dev signed root metadata"""
177        signed_root = SignedRootMetadata()
178        root_metadata_proto = self.generate_dev_root_metadata()
179        signed_root.serialized_root_metadata = (
180            root_metadata_proto.SerializeToString()
181        )
182        return dev_sign.sign_root_metadata(
183            signed_root, private_key_private_pem_bytes(self._root_dev_key)
184        )
185
186    def generate_prod_signed_root_metadata(
187        self, root_metadata_proto: RootMetadata | None = None
188    ) -> SignedRootMetadata:
189        """Generates a root metadata signed by the prod key"""
190        if not root_metadata_proto:
191            root_metadata_proto = self.generate_prod_root_metadata()
192
193        signed_root = SignedRootMetadata(
194            serialized_root_metadata=root_metadata_proto.SerializeToString()
195        )
196
197        return dev_sign.sign_root_metadata(
198            signed_root, private_key_private_pem_bytes(self._root_prod_key)
199        )
200
201    def generate_targets_metadata(self) -> TargetsMetadata:
202        """Generates the targets metadata"""
203        targets = metadata.gen_targets_metadata(
204            self._payloads, metadata.DEFAULT_HASHES, TEST_TARGETS_VERSION
205        )
206        return targets
207
208    def generate_unsigned_bundle(
209        self,
210        targets_metadata: TargetsMetadata | None = None,
211        signed_root_metadata: SignedRootMetadata | None = None,
212    ) -> UpdateBundle:
213        """Generate an unsigned (targets metadata) update bundle"""
214        bundle = UpdateBundle()
215
216        if not targets_metadata:
217            targets_metadata = self.generate_targets_metadata()
218
219        if signed_root_metadata:
220            bundle.root_metadata.CopyFrom(signed_root_metadata)
221
222        bundle.targets_metadata['targets'].CopyFrom(
223            SignedTargetsMetadata(
224                serialized_targets_metadata=targets_metadata.SerializeToString()
225            )
226        )
227
228        for name, payload in self._payloads.items():
229            bundle.target_payloads[name] = payload
230
231        return bundle
232
233    def generate_dev_signed_bundle(
234        self,
235        targets_metadata_override: TargetsMetadata | None = None,
236        signed_root_metadata: SignedRootMetadata | None = None,
237    ) -> UpdateBundle:
238        """Generate a dev signed update bundle"""
239        return dev_sign.sign_update_bundle(
240            self.generate_unsigned_bundle(
241                targets_metadata_override, signed_root_metadata
242            ),
243            private_key_private_pem_bytes(self._targets_dev_key),
244        )
245
246    def generate_prod_signed_bundle(
247        self,
248        targets_metadata_override: TargetsMetadata | None = None,
249        signed_root_metadata: SignedRootMetadata | None = None,
250    ) -> UpdateBundle:
251        """Generate a prod signed update bundle"""
252        # The targets metadata in a prod signed bundle can only be verified
253        # by a prod signed root. Because it is signed by the prod targets key.
254        # The prod signed root however, can be verified by a dev root.
255        return dev_sign.sign_update_bundle(
256            self.generate_unsigned_bundle(
257                targets_metadata_override, signed_root_metadata
258            ),
259            private_key_private_pem_bytes(self._targets_prod_key),
260        )
261
262    def generate_manifest(self) -> Manifest:
263        """Generates the manifest"""
264        manifest = Manifest()
265        manifest.targets_metadata['targets'].CopyFrom(
266            self.generate_targets_metadata()
267        )
268        if USER_MANIFEST_FILE_NAME in self._payloads:
269            manifest.user_manifest = self._payloads[USER_MANIFEST_FILE_NAME]
270        return manifest
271
272
273def parse_args():
274    """Setup argparse."""
275    parser = argparse.ArgumentParser()
276    parser.add_argument(
277        "output_header", help="output path of the generated C header"
278    )
279    return parser.parse_args()
280
281
282# TODO: b/237580538 - Refactor the code so that each test bundle generation
283# is done in a separate function or script.
284# pylint: disable=too-many-locals
285def main() -> int:
286    """Main"""
287    args = parse_args()
288
289    test_bundle = Bundle()
290
291    dev_signed_root = test_bundle.generate_dev_signed_root_metadata()
292    dev_signed_bundle = test_bundle.generate_dev_signed_bundle()
293    dev_signed_bundle_with_root = test_bundle.generate_dev_signed_bundle(
294        signed_root_metadata=dev_signed_root
295    )
296    unsigned_bundle_with_root = test_bundle.generate_unsigned_bundle(
297        signed_root_metadata=dev_signed_root
298    )
299    manifest_proto = test_bundle.generate_manifest()
300    prod_signed_root = test_bundle.generate_prod_signed_root_metadata()
301    prod_signed_bundle = test_bundle.generate_prod_signed_bundle(
302        None, prod_signed_root
303    )
304    dev_signed_bundle_with_prod_root = test_bundle.generate_dev_signed_bundle(
305        signed_root_metadata=prod_signed_root
306    )
307
308    # Generates a prod root metadata that fails signature verification against
309    # the dev root (i.e. it has a bad prod signature). This is done by making
310    # a bad prod signature.
311    bad_prod_signature = test_bundle.generate_prod_root_metadata()
312    signed_bad_prod_signature = test_bundle.generate_prod_signed_root_metadata(
313        bad_prod_signature
314    )
315    # Compromises the signature.
316    signed_bad_prod_signature.signatures[0].sig = b'1' * 64
317    signed_bad_prod_signature_bundle = test_bundle.generate_prod_signed_bundle(
318        None, signed_bad_prod_signature
319    )
320
321    # Generates a prod root metadtata that fails to verify itself. Specifically,
322    # the prod signature cannot be verified by the key in the incoming root
323    # metadata. This is done by dev signing a prod root metadata.
324    # pylint: disable=line-too-long
325    signed_mismatched_root_key_and_signature = SignedRootMetadata(
326        serialized_root_metadata=test_bundle.generate_prod_root_metadata().SerializeToString()
327    )
328    # pylint: enable=line-too-long
329    dev_root_key = serialization.load_pem_private_key(
330        TEST_DEV_KEY.encode(), None
331    )
332    signature = keys.create_ecdsa_signature(
333        signed_mismatched_root_key_and_signature.serialized_root_metadata,
334        private_key_private_pem_bytes(dev_root_key),  # type: ignore
335    )
336    signed_mismatched_root_key_and_signature.signatures.append(signature)
337    mismatched_root_key_and_signature_bundle = (
338        test_bundle.generate_prod_signed_bundle(
339            None, signed_mismatched_root_key_and_signature
340        )
341    )
342
343    # Generates a prod root metadata with rollback attempt.
344    root_rollback = test_bundle.generate_prod_root_metadata()
345    root_rollback.common_metadata.version = TEST_ROOT_VERSION - 1
346    signed_root_rollback = test_bundle.generate_prod_signed_root_metadata(
347        root_rollback
348    )
349    root_rollback_bundle = test_bundle.generate_prod_signed_bundle(
350        None, signed_root_rollback
351    )
352
353    # Generates a bundle with a bad target signature.
354    bad_targets_siganture = test_bundle.generate_prod_signed_bundle(
355        None, prod_signed_root
356    )
357    # Compromises the signature.
358    bad_targets_siganture.targets_metadata['targets'].signatures[0].sig = (
359        b'1' * 64
360    )
361
362    # Generates a bundle with rollback attempt
363    targets_rollback = test_bundle.generate_targets_metadata()
364    targets_rollback.common_metadata.version = TEST_TARGETS_VERSION - 1
365    targets_rollback_bundle = test_bundle.generate_prod_signed_bundle(
366        targets_rollback, prod_signed_root
367    )
368
369    # Generate bundles with mismatched hash
370    mismatched_hash_targets_bundles = []
371    # Generate bundles with mismatched file length
372    mismatched_length_targets_bundles = []
373    # Generate bundles with missing hash
374    missing_hash_targets_bundles = []
375    # Generate bundles with personalized out payload
376    personalized_out_bundles = []
377    # For each of the two files in `TARGET_FILES`, we generate a number of
378    # bundles each of which modify the target in the following way
379    # respectively:
380    # 1. Compromise its sha256 hash value in the targets metadata, so as to
381    #    test hash verification logic.
382    # 2. Remove the hashes, to trigger verification failure cause by missing
383    #    hashes.
384    # 3. Compromise the file length in the targets metadata.
385    # 4. Remove the payload to emulate being personalized out, so as to test
386    #    that it does not cause verification failure.
387    for idx, payload_file in enumerate(TARGET_FILES.items()):
388        mismatched_hash_targets = test_bundle.generate_targets_metadata()
389        mismatched_hash_targets.target_files[idx].hashes[0].hash = b'0' * 32
390        mismatched_hash_targets_bundle = (
391            test_bundle.generate_prod_signed_bundle(
392                mismatched_hash_targets, prod_signed_root
393            )
394        )
395        mismatched_hash_targets_bundles.append(mismatched_hash_targets_bundle)
396
397        mismatched_length_targets = test_bundle.generate_targets_metadata()
398        mismatched_length_targets.target_files[idx].length = 1
399        mismatched_length_targets_bundle = (
400            test_bundle.generate_prod_signed_bundle(
401                mismatched_length_targets, prod_signed_root
402            )
403        )
404        mismatched_length_targets_bundles.append(
405            mismatched_length_targets_bundle
406        )
407
408        missing_hash_targets = test_bundle.generate_targets_metadata()
409        missing_hash_targets.target_files[idx].hashes.pop()
410        missing_hash_targets_bundle = test_bundle.generate_prod_signed_bundle(
411            missing_hash_targets, prod_signed_root
412        )
413        missing_hash_targets_bundles.append(missing_hash_targets_bundle)
414
415        file_name, _ = payload_file
416        personalized_out_bundle = test_bundle.generate_prod_signed_bundle(
417            None, prod_signed_root
418        )
419        personalized_out_bundle.target_payloads.pop(file_name)
420        personalized_out_bundles.append(personalized_out_bundle)
421
422    with open(args.output_header, 'w') as header:
423        header.write(HEADER)
424        header.write(
425            proto_array_declaration(dev_signed_bundle, 'kTestDevBundle')
426        )
427        header.write(
428            proto_array_declaration(
429                dev_signed_bundle_with_root, 'kTestDevBundleWithRoot'
430            )
431        )
432        header.write(
433            proto_array_declaration(
434                unsigned_bundle_with_root, 'kTestUnsignedBundleWithRoot'
435            )
436        )
437        header.write(
438            proto_array_declaration(
439                dev_signed_bundle_with_prod_root, 'kTestDevBundleWithProdRoot'
440            )
441        )
442        header.write(
443            proto_array_declaration(manifest_proto, 'kTestBundleManifest')
444        )
445        header.write(proto_array_declaration(dev_signed_root, 'kDevSignedRoot'))
446        header.write(
447            proto_array_declaration(prod_signed_bundle, 'kTestProdBundle')
448        )
449        header.write(
450            proto_array_declaration(
451                mismatched_root_key_and_signature_bundle,
452                'kTestMismatchedRootKeyAndSignature',
453            )
454        )
455        header.write(
456            proto_array_declaration(
457                signed_bad_prod_signature_bundle, 'kTestBadProdSignature'
458            )
459        )
460        header.write(
461            proto_array_declaration(
462                bad_targets_siganture, 'kTestBadTargetsSignature'
463            )
464        )
465        header.write(
466            proto_array_declaration(
467                targets_rollback_bundle, 'kTestTargetsRollback'
468            )
469        )
470        header.write(
471            proto_array_declaration(root_rollback_bundle, 'kTestRootRollback')
472        )
473
474        for idx, mismatched_hash_bundle in enumerate(
475            mismatched_hash_targets_bundles
476        ):
477            header.write(
478                proto_array_declaration(
479                    mismatched_hash_bundle,
480                    f'kTestBundleMismatchedTargetHashFile{idx}',
481                )
482            )
483
484        for idx, missing_hash_bundle in enumerate(missing_hash_targets_bundles):
485            header.write(
486                proto_array_declaration(
487                    missing_hash_bundle,
488                    f'kTestBundleMissingTargetHashFile{idx}',
489                )
490            )
491
492        for idx, mismatched_length_bundle in enumerate(
493            mismatched_length_targets_bundles
494        ):
495            header.write(
496                proto_array_declaration(
497                    mismatched_length_bundle,
498                    f'kTestBundleMismatchedTargetLengthFile{idx}',
499                )
500            )
501
502        for idx, personalized_out_bundle in enumerate(personalized_out_bundles):
503            header.write(
504                proto_array_declaration(
505                    personalized_out_bundle,
506                    f'kTestBundlePersonalizedOutFile{idx}',
507                )
508            )
509    subprocess.run(
510        [
511            'clang-format',
512            '-i',
513            args.output_header,
514        ],
515        check=True,
516    )
517    return 0
518
519
520# TODO: b/237580538 - Refactor the code so that each test bundle generation
521# is done in a separate function or script.
522# pylint: enable=too-many-locals
523
524
525if __name__ == "__main__":
526    sys.exit(main())
527