blob: 8bc83b037c742cd219957c953a1bc8c592077fe9 [file] [log] [blame]
# Copyright 2021 The Pigweed Authors
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may not
# use this file except in compliance with the License. You may obtain a copy of
# the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations under
# the License.
"""Script for generating test bundles"""
import argparse
import subprocess
import sys
from typing import Dict
from pw_software_update import dev_sign, keys, metadata, root_metadata
from pw_software_update.update_bundle_pb2 import Manifest, UpdateBundle
from pw_software_update.tuf_pb2 import (RootMetadata, SignedRootMetadata,
TargetsMetadata, SignedTargetsMetadata)
from cryptography.hazmat.primitives.asymmetric import ec
from cryptography.hazmat.primitives import serialization
HEADER = """// Copyright 2021 The Pigweed Authors
//
// Licensed under the Apache License, Version 2.0 (the "License"); you may not
// use this file except in compliance with the License. You may obtain a copy
// of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
// License for the specific language governing permissions and limitations under
// the License.
#pragma once
#include "pw_bytes/span.h"
"""
TEST_DEV_KEY = """-----BEGIN PRIVATE KEY-----
MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgVgMQBOTJyx1xOafy
WTs2VkACf7Uo3RbP9Vun+oKXtMihRANCAATV7XJljxeUs2z2wqM5Q/kohAra1620
zXT90N9a3UR+IHksTd1OA7wFq220IQB/e4eVzbcOprN0MMMuSgXMxL8p
-----END PRIVATE KEY-----"""
TEST_PROD_KEY = """-----BEGIN PRIVATE KEY-----
MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQg73MLNmB/fPNX75Pl
YdynPtJkM2gGOWfIcHDuwuxSQmqhRANCAARpvjrXkjG2Fp+ZgREtxeTBBmJmWGS9
8Ny2tXY+Qggzl77G7wvCNF5+koz7ecsV6sKjK+dFiAXOIdqlga7p2j0A
-----END PRIVATE KEY-----"""
TEST_TARGETS_DEV_KEY = """-----BEGIN PRIVATE KEY-----
MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQggRCrido5vZOnkULH
sxQDt9Qoe/TlEKoqa1bhO1HFbi6hRANCAASVwdXbGWM7+f/r+Z2W6Dbd7CQA0Cbb
pkBv5PnA+DZnCkFhLW2kTn89zQv8W1x4m9maoINp9QPXQ4/nXlrVHqDg
-----END PRIVATE KEY-----"""
TEST_TARGETS_PROD_KEY = """-----BEGIN PRIVATE KEY-----
MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgx2VdB2EsUKghuLMG
RmxzqX2jnLTq5pxsFgO5Rrf5jlehRANCAASVijeDpemxVSlgZOOW0yvwE5QkXkq0
geWonkusMP0+MXopnmN0QlpgaCnG40TSr/W+wFjRmNCklL4dXk01oCwD
-----END PRIVATE KEY-----"""
TEST_ROOT_VERSION = 2
TEST_TARGETS_VERSION = 2
USER_MANIFEST_FILE_NAME = 'user_manifest'
TARGET_FILES = {
'file1': 'file 1 content'.encode(),
'file2': 'file 2 content'.encode(),
USER_MANIFEST_FILE_NAME: 'user manfiest content'.encode(),
}
def byte_array_declaration(data: bytes, name: str) -> str:
"""Generates a byte C array declaration for a byte array"""
type_name = '[[maybe_unused]] const std::byte'
byte_str = ''.join([f'std::byte{{0x{b:02x}}},' for b in data])
array_body = f'{{{byte_str}}}'
return f'{type_name} {name}[] = {array_body};'
def proto_array_declaration(proto, name: str) -> str:
"""Generates a byte array declaration for a proto"""
return byte_array_declaration(proto.SerializeToString(), name)
def private_key_public_pem_bytes(key: ec.EllipticCurvePrivateKey) -> bytes:
"""Serializes the public part of a private key in PEM format"""
return key.public_key().public_bytes(
serialization.Encoding.PEM,
serialization.PublicFormat.SubjectPublicKeyInfo)
def private_key_private_pem_bytes(key: ec.EllipticCurvePrivateKey) -> bytes:
"""Serializes the private part of a private key in PEM format"""
return key.private_bytes(encoding=serialization.Encoding.PEM,
format=serialization.PrivateFormat.PKCS8,
encryption_algorithm=serialization.NoEncryption())
class Bundle:
"""A helper for test UpdateBundle generation"""
def __init__(self):
self._root_dev_key = serialization.load_pem_private_key(
TEST_DEV_KEY.encode(), None)
self._root_prod_key = serialization.load_pem_private_key(
TEST_PROD_KEY.encode(), None)
self._targets_dev_key = serialization.load_pem_private_key(
TEST_TARGETS_DEV_KEY.encode(), None)
self._targets_prod_key = serialization.load_pem_private_key(
TEST_TARGETS_PROD_KEY.encode(), None)
self._payloads: Dict[str, bytes] = {}
# Adds some update files.
for key, value in TARGET_FILES.items():
self.add_payload(key, value)
def add_payload(self, name: str, payload: bytes) -> None:
"""Adds a payload to the bundle"""
self._payloads[name] = payload
def generate_dev_root_metadata(self) -> RootMetadata:
"""Generates a root metadata with the dev key"""
# The dev root metadata contains both the prod and the dev public key,
# so that it can rotate to prod. But it will only use a dev targets
# key.
return root_metadata.gen_root_metadata(
root_metadata.RootKeys([
private_key_public_pem_bytes(self._root_dev_key),
private_key_public_pem_bytes(self._root_prod_key),
]),
root_metadata.TargetsKeys(
[private_key_public_pem_bytes(self._targets_dev_key)]),
TEST_ROOT_VERSION)
def generate_prod_root_metadata(self) -> RootMetadata:
"""Generates a root metadata with the prod key"""
# The prod root metadta contains only the prod public key and uses the
# prod targets key
return root_metadata.gen_root_metadata(
root_metadata.RootKeys(
[private_key_public_pem_bytes(self._root_prod_key)]),
root_metadata.TargetsKeys(
[private_key_public_pem_bytes(self._targets_prod_key)]),
TEST_ROOT_VERSION)
def generate_dev_signed_root_metadata(self) -> SignedRootMetadata:
"""Generates a dev signed root metadata"""
signed_root = SignedRootMetadata()
root_metadata_proto = self.generate_dev_root_metadata()
signed_root.serialized_root_metadata = \
root_metadata_proto.SerializeToString()
return dev_sign.sign_root_metadata(
signed_root, private_key_private_pem_bytes(self._root_dev_key))
def generate_prod_signed_root_metadata(
self,
root_metadata_proto: RootMetadata = None) -> SignedRootMetadata:
"""Generates a root metadata signed by the prod key"""
if not root_metadata_proto:
root_metadata_proto = self.generate_prod_root_metadata()
signed_root = SignedRootMetadata(
serialized_root_metadata=root_metadata_proto.SerializeToString())
return dev_sign.sign_root_metadata(
signed_root, private_key_private_pem_bytes(self._root_prod_key))
def generate_targets_metadata(self) -> TargetsMetadata:
"""Generates the targets metadata"""
targets = metadata.gen_targets_metadata(self._payloads,
metadata.DEFAULT_HASHES,
TEST_TARGETS_VERSION)
return targets
def generate_unsigned_bundle(
self,
targets_metadata: TargetsMetadata = None,
signed_root_metadata: SignedRootMetadata = None) -> UpdateBundle:
"""Generate an unsigned (targets metadata) update bundle"""
bundle = UpdateBundle()
if not targets_metadata:
targets_metadata = self.generate_targets_metadata()
if signed_root_metadata:
bundle.root_metadata.CopyFrom(signed_root_metadata)
bundle.targets_metadata['targets'].CopyFrom(
SignedTargetsMetadata(serialized_targets_metadata=targets_metadata.
SerializeToString()))
for name, payload in self._payloads.items():
bundle.target_payloads[name] = payload
return bundle
def generate_dev_signed_bundle(
self,
targets_metadata_override: TargetsMetadata = None,
signed_root_metadata: SignedRootMetadata = None) -> UpdateBundle:
"""Generate a dev signed update bundle"""
return dev_sign.sign_update_bundle(
self.generate_unsigned_bundle(targets_metadata_override,
signed_root_metadata),
private_key_private_pem_bytes(self._targets_dev_key))
def generate_prod_signed_bundle(
self,
targets_metadata_override: TargetsMetadata = None,
signed_root_metadata: SignedRootMetadata = None) -> UpdateBundle:
"""Generate a prod signed update bundle"""
# The targets metadata in a prod signed bundle can only be verified
# by a prod signed root. Because it is signed by the prod targets key.
# The prod signed root however, can be verified by a dev root.
return dev_sign.sign_update_bundle(
self.generate_unsigned_bundle(targets_metadata_override,
signed_root_metadata),
private_key_private_pem_bytes(self._targets_prod_key))
def generate_manifest(self) -> Manifest:
"""Generates the manifest"""
manifest = Manifest()
manifest.targets_metadata['targets'].CopyFrom(
self.generate_targets_metadata())
if USER_MANIFEST_FILE_NAME in self._payloads:
manifest.user_manifest = self._payloads[USER_MANIFEST_FILE_NAME]
return manifest
def parse_args():
"""Setup argparse."""
parser = argparse.ArgumentParser()
parser.add_argument("output_header",
help="output path of the generated C header")
return parser.parse_args()
def main() -> int:
"""Main"""
# TODO(b/237580538): Refactor the code so that each test bundle generation
# is done in a separate function or script.
# pylint: disable=too-many-locals
args = parse_args()
test_bundle = Bundle()
dev_signed_root = test_bundle.generate_dev_signed_root_metadata()
dev_signed_bundle = test_bundle.generate_dev_signed_bundle()
dev_signed_bundle_with_root = test_bundle.generate_dev_signed_bundle(
signed_root_metadata=dev_signed_root)
unsigned_bundle_with_root = test_bundle.generate_unsigned_bundle(
signed_root_metadata=dev_signed_root)
manifest_proto = test_bundle.generate_manifest()
prod_signed_root = \
test_bundle.generate_prod_signed_root_metadata()
prod_signed_bundle = test_bundle.generate_prod_signed_bundle(
None, prod_signed_root)
dev_signed_bundle_with_prod_root = test_bundle.generate_dev_signed_bundle(
signed_root_metadata=prod_signed_root)
# Generates a prod root metadata that fails signature verification against
# the dev root (i.e. it has a bad prod signature). This is done by making
# a bad prod signature.
bad_prod_signature = test_bundle.generate_prod_root_metadata()
signed_bad_prod_signature = \
test_bundle\
.generate_prod_signed_root_metadata(
bad_prod_signature)
# Compromises the signature.
signed_bad_prod_signature.signatures[0].sig = b'1' * 64
signed_bad_prod_signature_bundle = test_bundle.generate_prod_signed_bundle(
None, signed_bad_prod_signature)
# Generates a prod root metadtata that fails to verify itself. Specifically,
# the prod signature cannot be verified by the key in the incoming root
# metadata. This is done by dev signing a prod root metadata.
signed_mismatched_root_key_and_signature = SignedRootMetadata(
serialized_root_metadata=test_bundle.generate_prod_root_metadata(
).SerializeToString())
dev_root_key = serialization.load_pem_private_key(TEST_DEV_KEY.encode(),
None)
signature = keys.create_ecdsa_signature(
signed_mismatched_root_key_and_signature.serialized_root_metadata,
private_key_private_pem_bytes(dev_root_key)) # type: ignore
signed_mismatched_root_key_and_signature.signatures.append(signature)
mismatched_root_key_and_signature_bundle = test_bundle\
.generate_prod_signed_bundle(None,
signed_mismatched_root_key_and_signature)
# Generates a prod root metadata with rollback attempt.
root_rollback = test_bundle.generate_prod_root_metadata()
root_rollback.common_metadata.version = TEST_ROOT_VERSION - 1
signed_root_rollback = test_bundle.\
generate_prod_signed_root_metadata(root_rollback)
root_rollback_bundle = test_bundle.generate_prod_signed_bundle(
None, signed_root_rollback)
# Generates a bundle with a bad target signature.
bad_targets_siganture = test_bundle.generate_prod_signed_bundle(
None, prod_signed_root)
# Compromises the signature.
bad_targets_siganture.targets_metadata['targets'].signatures[
0].sig = b'1' * 64
# Generates a bundle with rollback attempt
targets_rollback = test_bundle.generate_targets_metadata()
targets_rollback.common_metadata.version = TEST_TARGETS_VERSION - 1
targets_rollback_bundle = test_bundle.generate_prod_signed_bundle(
targets_rollback, prod_signed_root)
# Generate bundles with mismatched hash
mismatched_hash_targets_bundles = []
# Generate bundles with mismatched file length
mismatched_length_targets_bundles = []
# Generate bundles with missing hash
missing_hash_targets_bundles = []
# Generate bundles with personalized out payload
personalized_out_bundles = []
# For each of the two files in `TARGET_FILES`, we generate a number of
# bundles each of which modify the target in the following way
# respectively:
# 1. Compromise its sha256 hash value in the targets metadata, so as to
# test hash verification logic.
# 2. Remove the hashes, to trigger verification failure cause by missing
# hashes.
# 3. Compromise the file length in the targets metadata.
# 4. Remove the payload to emulate being personalized out, so as to test
# that it does not cause verification failure.
for idx, payload_file in enumerate(TARGET_FILES.items()):
mismatched_hash_targets = test_bundle.generate_targets_metadata()
mismatched_hash_targets.target_files[idx].hashes[0].hash = b'0' * 32
mismatched_hash_targets_bundle = test_bundle\
.generate_prod_signed_bundle(
mismatched_hash_targets, prod_signed_root)
mismatched_hash_targets_bundles.append(mismatched_hash_targets_bundle)
mismatched_length_targets = test_bundle.generate_targets_metadata()
mismatched_length_targets.target_files[idx].length = 1
mismatched_length_targets_bundle = test_bundle\
.generate_prod_signed_bundle(
mismatched_length_targets, prod_signed_root)
mismatched_length_targets_bundles.append(
mismatched_length_targets_bundle)
missing_hash_targets = test_bundle.generate_targets_metadata()
missing_hash_targets.target_files[idx].hashes.pop()
missing_hash_targets_bundle = test_bundle.generate_prod_signed_bundle(
missing_hash_targets, prod_signed_root)
missing_hash_targets_bundles.append(missing_hash_targets_bundle)
file_name, _ = payload_file
personalized_out_bundle = test_bundle.generate_prod_signed_bundle(
None, prod_signed_root)
personalized_out_bundle.target_payloads.pop(file_name)
personalized_out_bundles.append(personalized_out_bundle)
with open(args.output_header, 'w') as header:
header.write(HEADER)
header.write(
proto_array_declaration(dev_signed_bundle, 'kTestDevBundle'))
header.write(
proto_array_declaration(dev_signed_bundle_with_root,
'kTestDevBundleWithRoot'))
header.write(
proto_array_declaration(unsigned_bundle_with_root,
'kTestUnsignedBundleWithRoot'))
header.write(
proto_array_declaration(dev_signed_bundle_with_prod_root,
'kTestDevBundleWithProdRoot'))
header.write(
proto_array_declaration(manifest_proto, 'kTestBundleManifest'))
header.write(proto_array_declaration(dev_signed_root,
'kDevSignedRoot'))
header.write(
proto_array_declaration(prod_signed_bundle, 'kTestProdBundle'))
header.write(
proto_array_declaration(mismatched_root_key_and_signature_bundle,
'kTestMismatchedRootKeyAndSignature'))
header.write(
proto_array_declaration(signed_bad_prod_signature_bundle,
'kTestBadProdSignature'))
header.write(
proto_array_declaration(bad_targets_siganture,
'kTestBadTargetsSignature'))
header.write(
proto_array_declaration(targets_rollback_bundle,
'kTestTargetsRollback'))
header.write(
proto_array_declaration(root_rollback_bundle, 'kTestRootRollback'))
for idx, mismatched_hash_bundle in enumerate(
mismatched_hash_targets_bundles):
header.write(
proto_array_declaration(
mismatched_hash_bundle,
f'kTestBundleMismatchedTargetHashFile{idx}'))
for idx, missing_hash_bundle in enumerate(
missing_hash_targets_bundles):
header.write(
proto_array_declaration(
missing_hash_bundle,
f'kTestBundleMissingTargetHashFile{idx}'))
for idx, mismatched_length_bundle in enumerate(
mismatched_length_targets_bundles):
header.write(
proto_array_declaration(
mismatched_length_bundle,
f'kTestBundleMismatchedTargetLengthFile{idx}'))
for idx, personalized_out_bundle in enumerate(
personalized_out_bundles):
header.write(
proto_array_declaration(
personalized_out_bundle,
f'kTestBundlePersonalizedOutFile{idx}'))
subprocess.run([
'clang-format',
'-i',
args.output_header,
], check=True)
# TODO(b/237580538): Refactor the code so that each test bundle generation
# is done in a separate function or script.
# pylint: enable=too-many-locals
return 0
if __name__ == "__main__":
sys.exit(main())