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