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