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