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