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