xref: /aosp_15_r20/external/pigweed/pw_software_update/py/verify_test.py (revision 61c4878ac05f98d0ceed94b57d316916de578985)
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"""Unit tests for pw_software_update/dev_sign.py."""
15
16from dataclasses import dataclass
17from pathlib import Path
18import tempfile
19from typing import NamedTuple
20import unittest
21
22from pw_software_update import dev_sign, root_metadata, update_bundle
23from pw_software_update.verify import VerificationError, verify_bundle
24from pw_software_update.tuf_pb2 import SignedRootMetadata
25from pw_software_update.update_bundle_pb2 import UpdateBundle
26
27
28def gen_unsigned_bundle(
29    signed_root_metadata: SignedRootMetadata | None = None,
30    targets_metadata_version: int = 0,
31) -> UpdateBundle:
32    """Generates an unsigned test bundle."""
33    with tempfile.TemporaryDirectory() as tempdir_name:
34        targets_root = Path(tempdir_name)
35        foo_path = targets_root / 'foo.bin'
36        bar_path = targets_root / 'bar.bin'
37        baz_path = targets_root / 'baz.bin'
38        qux_path = targets_root / 'subdir' / 'qux.exe'
39        foo_bytes = b'\xf0\x0b\xa4'
40        bar_bytes = b'\x0b\xa4\x99'
41        baz_bytes = b'\xba\x59\x06'
42        qux_bytes = b'\x8a\xf3\x12'
43        foo_path.write_bytes(foo_bytes)
44        bar_path.write_bytes(bar_bytes)
45        baz_path.write_bytes(baz_bytes)
46        (targets_root / 'subdir').mkdir()
47        qux_path.write_bytes(qux_bytes)
48        targets = {
49            foo_path: 'foo',
50            bar_path: 'bar',
51            baz_path: 'baz',
52            qux_path: 'qux',
53        }
54        return update_bundle.gen_unsigned_update_bundle(
55            targets,
56            root_metadata=signed_root_metadata,
57            targets_metadata_version=targets_metadata_version,
58        )
59
60
61class TestKey(NamedTuple):
62    """A test key pair"""
63
64    public: bytes
65    private: bytes
66
67
68@dataclass
69class BundleOptions:
70    """Parameters used in test bundle generations."""
71
72    root_key_version: int = 0
73    root_metadata_version: int = 0
74    targets_key_version: int = 0
75    targets_metadata_version: int = 0
76
77
78def gen_signed_bundle(options: BundleOptions) -> UpdateBundle:
79    """Generates a test bundle per given options."""
80    # Root keys look up table: version->TestKey
81    root_keys = {
82        0: TestKey(
83            private=(
84                b'-----BEGIN PRIVATE KEY-----\n'
85                b'MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgyk3DEQdl'
86                b'346MS5N/quNEneJa4HxkJBETGzlEEKkCmZOhRANCAAThdY5PejbtM2p6'
87                b'HtgXs/7YSsvPMWZz9Ui1gAEKrDseHnPzC02MbKjQadRIFZ4hKDcsyz9a'
88                b'M6QKLCNrCOqYjw6t'
89                b'\n-----END PRIVATE KEY-----\n'
90            ),
91            public=(
92                b'-----BEGIN PUBLIC KEY-----\n'
93                b'MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE4XWOT3o27TNqeh7YF7P+2'
94                b'ErLzzFmc/VItYABCqw7Hh5z8wtNjGyo0GnUSBWeISg3LMs/WjOkCiwjaw'
95                b'jqmI8OrQ=='
96                b'\n-----END PUBLIC KEY-----\n'
97            ),
98        ),
99        1: TestKey(
100            private=(
101                b'-----BEGIN PRIVATE KEY-----\n'
102                b'MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgE3MRbMxo'
103                b'Gv3I/Ok/0qE8GV/mQuIbZo9kk+AsJnYetQ6hRANCAAQ5UhycwdcfYe34'
104                b'NpmG32t0klnKlrUbk3LyvYLq5uDWG2MfP3L0ciNFsEnW7vHpqqjKsoru'
105                b'Qt30G10K7D+reC77'
106                b'\n-----END PRIVATE KEY-----\n'
107            ),
108            public=(
109                b'-----BEGIN PUBLIC KEY-----\n'
110                b'MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEOVIcnMHXH2Ht+DaZht9rd'
111                b'JJZypa1G5Ny8r2C6ubg1htjHz9y9HIjRbBJ1u7x6aqoyrKK7kLd9BtdCu'
112                b'w/q3gu+w=='
113                b'\n-----END PUBLIC KEY-----\n'
114            ),
115        ),
116    }
117
118    # Targets keys look up table: version->TestKey
119    targets_keys = {
120        0: TestKey(
121            private=(
122                b'-----BEGIN PRIVATE KEY-----\n'
123                b'MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgkMEZ0u84'
124                b'HzC51nhhf2ZykPj6WfAjBxXVWndjVdn6bh6hRANCAAT1QzqpFknSAhbA'
125                b'uOjy2NuusFOUpeC6TBWM6WeC5JKJgys3gwOoyU0OdomAu9wK6I1Qoe70'
126                b'6PUMbWLpyQ10ThVM'
127                b'\n-----END PRIVATE KEY-----\n'
128            ),
129            public=(
130                b'-----BEGIN PUBLIC KEY-----\n'
131                b'MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE9UM6qRZJ0gIWwLjo8tjbr'
132                b'rBTlKXgukwVjOlnguSSiYMrN4MDqMlNDnaJgLvcCuiNUKHu9Oj1DG1i6c'
133                b'kNdE4VTA=='
134                b'\n-----END PUBLIC KEY-----\n'
135            ),
136        ),
137        1: TestKey(
138            private=(
139                b'-----BEGIN PRIVATE KEY-----\n'
140                b'MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQg+Q+u2KoO'
141                b'CwpY1HEKDTIjQXmTlxhoo3gVkE7nrtHhMemhRANCAASgc+0AHCfUxoHy'
142                b'+ZkSslLvMufiDqGPABvfuKzHd0wUWs2Y0eIvQc7tsBP0bcuJsFuxvL6a'
143                b'8Ek7y3kUmFWVL01v'
144                b'\n-----END PRIVATE KEY-----\n'
145            ),
146            public=(
147                b'-----BEGIN PUBLIC KEY-----\n'
148                b'MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEoHPtABwn1MaB8vmZErJS7'
149                b'zLn4g6hjwAb37isx3dMFFrNmNHiL0HO7bAT9G3LibBbsby+mvBJO8t5FJ'
150                b'hVlS9Nbw=='
151                b'\n-----END PUBLIC KEY-----\n'
152            ),
153        ),
154    }
155
156    unsigned_root = root_metadata.gen_root_metadata(
157        root_metadata.RootKeys([root_keys[options.root_key_version].public]),
158        root_metadata.TargetsKeys(
159            [targets_keys[options.targets_key_version].public]
160        ),
161        version=options.root_metadata_version,
162    )
163
164    serialized_root = unsigned_root.SerializeToString()
165    signed_root = SignedRootMetadata(serialized_root_metadata=serialized_root)
166    signed_root = dev_sign.sign_root_metadata(
167        signed_root, root_keys[options.root_key_version].private
168    )
169    # Additionaly sign the root metadata with the previous version of root key
170    # to enable upgrading from the previous root.
171    if options.root_key_version > 0:
172        signed_root = dev_sign.sign_root_metadata(
173            signed_root, root_keys[options.root_key_version - 1].private
174        )
175
176    unsigned_bundle = gen_unsigned_bundle(
177        signed_root_metadata=signed_root,
178        targets_metadata_version=options.targets_metadata_version,
179    )
180    signed_bundle = dev_sign.sign_update_bundle(
181        unsigned_bundle, targets_keys[options.targets_key_version].private
182    )
183
184    return signed_bundle
185
186
187class VerifyBundleTest(unittest.TestCase):
188    """Bundle verification test cases."""
189
190    def test_self_verification(self):  # pylint: disable=no-self-use
191        incoming = gen_signed_bundle(BundleOptions())
192        verify_bundle(incoming, trusted=incoming)
193
194    def test_root_key_rotation(self):  # pylint: disable=no-self-use
195        trusted = gen_signed_bundle(BundleOptions(root_key_version=0))
196        incoming = gen_signed_bundle(BundleOptions(root_key_version=1))
197        verify_bundle(incoming, trusted)
198
199    def test_root_metadata_anti_rollback(self):
200        trusted = gen_signed_bundle(BundleOptions(root_metadata_version=1))
201        incoming = gen_signed_bundle(BundleOptions(root_metadata_version=0))
202        with self.assertRaises(VerificationError):
203            verify_bundle(incoming, trusted)
204
205    def test_root_metadata_anti_rollback_with_key_rotation(self):
206        trusted = gen_signed_bundle(
207            BundleOptions(root_key_version=0, root_metadata_version=1)
208        )
209        incoming = gen_signed_bundle(
210            BundleOptions(root_key_version=1, root_metadata_version=0)
211        )
212        # Anti-rollback enforced regardless of key rotation.
213        with self.assertRaises(VerificationError):
214            verify_bundle(incoming, trusted)
215
216    def test_missing_root(self):
217        incoming = gen_signed_bundle(BundleOptions())
218        incoming.ClearField('root_metadata')
219        with self.assertRaises(VerificationError):
220            verify_bundle(incoming, trusted=incoming)
221
222    def test_targets_key_rotation(self):  # pylint: disable=no-self-use
223        trusted = gen_signed_bundle(BundleOptions(targets_key_version=0))
224        incoming = gen_signed_bundle(BundleOptions(targets_key_version=1))
225        verify_bundle(incoming, trusted)
226
227    def test_targets_metadata_anti_rollback(self):
228        trusted = gen_signed_bundle(BundleOptions(targets_metadata_version=1))
229        incoming = gen_signed_bundle(BundleOptions(targets_metadata_version=0))
230        with self.assertRaises(VerificationError):
231            verify_bundle(incoming, trusted)
232
233    def test_targets_fastforward_recovery(self):  # pylint: disable=no-self-use
234        trusted = gen_signed_bundle(
235            BundleOptions(targets_key_version=0, targets_metadata_version=999)
236        )
237        # Revoke key and bring back the metadata version.
238        incoming = gen_signed_bundle(
239            BundleOptions(targets_key_version=1, targets_metadata_version=0)
240        )
241        # Anti-rollback is not enforced upon key rotation.
242        verify_bundle(incoming, trusted)
243
244
245if __name__ == '__main__':
246    unittest.main()
247