blob: a19cee0c5c8f7f434c4ad76c4106b4a275f6a117 [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.
"""Unit tests for pw_software_update/dev_sign.py."""
from dataclasses import dataclass
from pathlib import Path
import tempfile
from typing import NamedTuple
import unittest
from pw_software_update import dev_sign, root_metadata, update_bundle
from pw_software_update.verify import VerificationError, verify_bundle
from pw_software_update.tuf_pb2 import SignedRootMetadata
from pw_software_update.update_bundle_pb2 import UpdateBundle
def gen_unsigned_bundle(signed_root_metadata: SignedRootMetadata = None,
targets_metadata_version: int = 0) -> UpdateBundle:
"""Generates an unsigned test bundle."""
with tempfile.TemporaryDirectory() as tempdir_name:
targets_root = Path(tempdir_name)
foo_path = targets_root / 'foo.bin'
bar_path = targets_root / 'bar.bin'
baz_path = targets_root / 'baz.bin'
qux_path = targets_root / 'subdir' / 'qux.exe'
foo_bytes = b'\xf0\x0b\xa4'
bar_bytes = b'\x0b\xa4\x99'
baz_bytes = b'\xba\x59\x06'
qux_bytes = b'\x8a\xf3\x12'
foo_path.write_bytes(foo_bytes)
bar_path.write_bytes(bar_bytes)
baz_path.write_bytes(baz_bytes)
(targets_root / 'subdir').mkdir()
qux_path.write_bytes(qux_bytes)
targets = {
foo_path: 'foo',
bar_path: 'bar',
baz_path: 'baz',
qux_path: 'qux',
}
return update_bundle.gen_unsigned_update_bundle(
targets,
root_metadata=signed_root_metadata,
targets_metadata_version=targets_metadata_version)
class TestKey(NamedTuple):
"""A test key pair"""
public: bytes
private: bytes
@dataclass
class BundleOptions:
"""Parameters used in test bundle generations."""
root_key_version: int = 0
root_metadata_version: int = 0
targets_key_version: int = 0
targets_metadata_version: int = 0
def gen_signed_bundle(options: BundleOptions) -> UpdateBundle:
"""Generates a test bundle per given options."""
# Root keys look up table: version->TestKey
root_keys = {
0:
TestKey(
private=(
b'-----BEGIN PRIVATE KEY-----\n'
b'MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgyk3DEQdl'
b'346MS5N/quNEneJa4HxkJBETGzlEEKkCmZOhRANCAAThdY5PejbtM2p6'
b'HtgXs/7YSsvPMWZz9Ui1gAEKrDseHnPzC02MbKjQadRIFZ4hKDcsyz9a'
b'M6QKLCNrCOqYjw6t'
b'\n-----END PRIVATE KEY-----\n'),
public=(
b'-----BEGIN PUBLIC KEY-----\n'
b'MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE4XWOT3o27TNqeh7YF7P+2'
b'ErLzzFmc/VItYABCqw7Hh5z8wtNjGyo0GnUSBWeISg3LMs/WjOkCiwjaw'
b'jqmI8OrQ=='
b'\n-----END PUBLIC KEY-----\n')),
1:
TestKey(
private=(
b'-----BEGIN PRIVATE KEY-----\n'
b'MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgE3MRbMxo'
b'Gv3I/Ok/0qE8GV/mQuIbZo9kk+AsJnYetQ6hRANCAAQ5UhycwdcfYe34'
b'NpmG32t0klnKlrUbk3LyvYLq5uDWG2MfP3L0ciNFsEnW7vHpqqjKsoru'
b'Qt30G10K7D+reC77'
b'\n-----END PRIVATE KEY-----\n'),
public=(
b'-----BEGIN PUBLIC KEY-----\n'
b'MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEOVIcnMHXH2Ht+DaZht9rd'
b'JJZypa1G5Ny8r2C6ubg1htjHz9y9HIjRbBJ1u7x6aqoyrKK7kLd9BtdCu'
b'w/q3gu+w=='
b'\n-----END PUBLIC KEY-----\n'))
}
# Targets keys look up table: version->TestKey
targets_keys = {
0:
TestKey(
private=(
b'-----BEGIN PRIVATE KEY-----\n'
b'MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgkMEZ0u84'
b'HzC51nhhf2ZykPj6WfAjBxXVWndjVdn6bh6hRANCAAT1QzqpFknSAhbA'
b'uOjy2NuusFOUpeC6TBWM6WeC5JKJgys3gwOoyU0OdomAu9wK6I1Qoe70'
b'6PUMbWLpyQ10ThVM'
b'\n-----END PRIVATE KEY-----\n'),
public=(
b'-----BEGIN PUBLIC KEY-----\n'
b'MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE9UM6qRZJ0gIWwLjo8tjbr'
b'rBTlKXgukwVjOlnguSSiYMrN4MDqMlNDnaJgLvcCuiNUKHu9Oj1DG1i6c'
b'kNdE4VTA=='
b'\n-----END PUBLIC KEY-----\n')),
1:
TestKey(
private=(
b'-----BEGIN PRIVATE KEY-----\n'
b'MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQg+Q+u2KoO'
b'CwpY1HEKDTIjQXmTlxhoo3gVkE7nrtHhMemhRANCAASgc+0AHCfUxoHy'
b'+ZkSslLvMufiDqGPABvfuKzHd0wUWs2Y0eIvQc7tsBP0bcuJsFuxvL6a'
b'8Ek7y3kUmFWVL01v'
b'\n-----END PRIVATE KEY-----\n'),
public=(
b'-----BEGIN PUBLIC KEY-----\n'
b'MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEoHPtABwn1MaB8vmZErJS7'
b'zLn4g6hjwAb37isx3dMFFrNmNHiL0HO7bAT9G3LibBbsby+mvBJO8t5FJ'
b'hVlS9Nbw=='
b'\n-----END PUBLIC KEY-----\n'))
}
unsigned_root = root_metadata.gen_root_metadata(
root_metadata.RootKeys([root_keys[options.root_key_version].public]),
root_metadata.TargetsKeys(
[targets_keys[options.targets_key_version].public]),
version=options.root_metadata_version)
serialized_root = unsigned_root.SerializeToString()
signed_root = SignedRootMetadata(serialized_root_metadata=serialized_root)
signed_root = dev_sign.sign_root_metadata(
signed_root, root_keys[options.root_key_version].private)
# Additionaly sign the root metadata with the previous version of root key
# to enable upgrading from the previous root.
if options.root_key_version > 0:
signed_root = dev_sign.sign_root_metadata(
signed_root, root_keys[options.root_key_version - 1].private)
unsigned_bundle = gen_unsigned_bundle(
signed_root_metadata=signed_root,
targets_metadata_version=options.targets_metadata_version)
signed_bundle = dev_sign.sign_update_bundle(
unsigned_bundle, targets_keys[options.targets_key_version].private)
return signed_bundle
class VerifyBundleTest(unittest.TestCase):
"""Bundle verification test cases."""
def test_self_verification(self): # pylint: disable=no-self-use
incoming = gen_signed_bundle(BundleOptions())
verify_bundle(incoming, trusted=incoming)
def test_root_key_rotation(self): # pylint: disable=no-self-use
trusted = gen_signed_bundle(BundleOptions(root_key_version=0))
incoming = gen_signed_bundle(BundleOptions(root_key_version=1))
verify_bundle(incoming, trusted)
def test_root_metadata_anti_rollback(self):
trusted = gen_signed_bundle(BundleOptions(root_metadata_version=1))
incoming = gen_signed_bundle(BundleOptions(root_metadata_version=0))
with self.assertRaises(VerificationError):
verify_bundle(incoming, trusted)
def test_root_metadata_anti_rollback_with_key_rotation(self):
trusted = gen_signed_bundle(
BundleOptions(root_key_version=0, root_metadata_version=1))
incoming = gen_signed_bundle(
BundleOptions(root_key_version=1, root_metadata_version=0))
# Anti-rollback enforced regardless of key rotation.
with self.assertRaises(VerificationError):
verify_bundle(incoming, trusted)
def test_missing_root(self):
incoming = gen_signed_bundle(BundleOptions())
incoming.ClearField('root_metadata')
with self.assertRaises(VerificationError):
verify_bundle(incoming, trusted=incoming)
def test_targets_key_rotation(self): # pylint: disable=no-self-use
trusted = gen_signed_bundle(BundleOptions(targets_key_version=0))
incoming = gen_signed_bundle(BundleOptions(targets_key_version=1))
verify_bundle(incoming, trusted)
def test_targets_metadata_anti_rollback(self):
trusted = gen_signed_bundle(BundleOptions(targets_metadata_version=1))
incoming = gen_signed_bundle(BundleOptions(targets_metadata_version=0))
with self.assertRaises(VerificationError):
verify_bundle(incoming, trusted)
def test_targets_fastforward_recovery(self): # pylint: disable=no-self-use
trusted = gen_signed_bundle(
BundleOptions(targets_key_version=0, targets_metadata_version=999))
# Revoke key and bring back the metadata version.
incoming = gen_signed_bundle(
BundleOptions(targets_key_version=1, targets_metadata_version=0))
# Anti-rollback is not enforced upon key rotation.
verify_bundle(incoming, trusted)
if __name__ == '__main__':
unittest.main()