1# Copyright (c) 2013 The Chromium OS Authors. All rights reserved.
2# Use of this source code is governed by a BSD-style license that can be
3# found in the LICENSE file.
4
5import os
6import urlparse
7
8from autotest_lib.client.common_lib import error
9from autotest_lib.client.common_lib.cros import dev_server
10from autotest_lib.server import autotest, test
11
12def _split_url(url):
13    """Splits a URL into the URL base and path."""
14    split_url = urlparse.urlsplit(url)
15    url_base = urlparse.urlunsplit(
16            (split_url.scheme, split_url.netloc, '', '', ''))
17    url_path = split_url.path
18    return url_base, url_path.lstrip('/')
19
20
21class autoupdate_CatchBadSignatures(test.test):
22    """This is a test to verify that update_engine correctly checks
23    signatures in the metadata hash and the update payload
24    itself. This is achieved by feeding updates to update_engine where
25    the private key used to make the signature, intentionally does not
26    match with the public key used for verification.
27
28    By its very nature, this test requires an image signed with a
29    well-known key. Since payload-generation is a resource-intensive
30    process, we prepare the image ahead of time. Also, since the image
31    is never successfully applied, we can get away with not caring that
32    the image is built for one board but used on another.
33
34    If you ever need to replace the test image, follow these eight
35    simple steps:
36
37    1. Build a test image:
38
39      $ cd ~/trunk/src/scripts
40      $ ./build_packages --board=${BOARD}
41      $ ./build_image --board=${BOARD} --noenable_rootfs_verification test
42
43    2. Serve the image the DUT like this:
44
45      $ cd ~/trunk/src/platform/dev
46      $ ./devserver.py --test_image                                             \
47                       --private_key                                            \
48                         ../../aosp/system/update_engine/unittest_key.pem       \
49                       --private_key_for_metadata_hash_signature                \
50                         ../../aosp/system/update_engine/unittest_key.pem       \
51                       --public_key                                             \
52                         ../../aosp/system/update_engine/unittest_key2.pub.pem
53
54      Note that unittest_key2.pub.pem can be generated via
55      $ openssl rsa                                                             \
56                -in ../../aosp/system/update_engine/unittest_key2.pem -pubout   \
57                -out ../../aosp/system/update_engine/unittest_key2.pub.pem
58
59    3. Update the DUT - the update should fail at the metadata
60       verification stage.
61
62    4. From the update_engine logs (stored in /var/log/update_engine/)
63       on the DUT, find the Omaha response sent to the DUT and update
64       the following constants with values from the XML:
65
66        _IMAGE_SHA256: set it to the 'sha256'
67        _IMAGE_METADATA_SIZE: set it to the 'MetadataSize'
68        _IMAGE_PUBLIC_KEY2: set it to the 'PublicKeyRsa'
69        _IMAGE_METADATA_SIGNATURE_WITH_KEY1: set it to 'MetadataSignatureRsa'
70
71       Also download the image payload (follow 'url' and 'codebase' tags for
72       directory then 'package' for the file name and its size),
73       upload it to Google Storage and update the _IMAGE_GS_URL and
74       _IMAGE_SIZE constants with the resulting URL and the size.
75
76    5. Serve the image to the DUT again and note the slightly different
77       parameters this time. Note that the image served is the same,
78       however the Omaha response will be different.
79
80      $ cd ~/trunk/src/platform/dev
81      $ ./devserver.py --test_image                                             \
82                       --private_key                                            \
83                         ../../aosp/system/update_engine/unittest_key.pem       \
84                       --private_key_for_metadata_hash_signature                \
85                         ../../aosp/system/update_engine/unittest_key2.pem      \
86                       --public_key                                             \
87                         ../../aosp/system/update_engine/unittest_key2.pub.pem
88
89    6. Update the DUT - the update should fail at the payload
90       verification stage.
91
92    7. Like in step 4., examine the update_engine logs and update the
93       following constants:
94
95        _IMAGE_METADATA_SIGNATURE_WITH_KEY2: set to 'MetadataSignatureRsa'
96
97    8. Now run the test and ensure that it passes
98
99      $ cd ~/trunk/src/scripts
100      $ test_that -b ${BOARD} --fast <DUT_IP> autoupdate_CatchBadSignatures
101
102    """
103    version = 1
104
105    # The test image to use and the values associated with it.
106    _IMAGE_GS_URL='gs://chromiumos-test-assets-public/autoupdate/autoupdate_CatchBadSignatures-payload-sentry-R54-8719.0.2016_08_18_2057-a1'
107    _IMAGE_SIZE=405569018
108    _IMAGE_METADATA_SIZE=49292
109    _IMAGE_SHA256='PHjzsTdCRMvMM7s+8f7K8ZegKoFnAf1UNhG6qc7zjRU='
110    _IMAGE_PUBLIC_KEY1='LS0tLS1CRUdJTiBQVUJMSUMgS0VZLS0tLS0KTUlJQklqQU5CZ2txaGtpRzl3MEJBUUVGQUFPQ0FROEFNSUlCQ2dLQ0FRRUF4NmhxUytIYmM3ak44MmVrK09CawpISk52bFdYa3R5UzJYQ3VRdEd0bkhqRVY3T3U1aEhORjk2czV3RW44UkR0cmRMb2NtMGErU3FYWGY3S3ljRlJUClp4TGREYnFXQU1VbFBYT2FQSStQWkxXa0I3L0tWN0NkajZ2UEdiZXE3ZWx1K2FUL2J1ZGh6VHZxMnN0WXJyQWwKY3IvMjF0T1ZEUGlXdGZDZHlraDdGQ1hpNkZhWUhvTnk1QTZFS1FMZkxCdUpvVS9Rb0N1ZmxkbXdsRmFGREtsKwpLb29zNlIxUVlKZkNOWmZnb2NyVzFQSWgrOHQxSkl2dzZJem84K2ZUbWU3bmV2N09sMllaaU1XSnBSWUt4OE1nCnhXMlVnVFhsUnBtUU41NnBjOUxVc25WQThGTkRCTjU3K2dNSmorWG1kRG1idE1wT3N5WGZTTkVnbnV3TVBjRWMKbXdJREFRQUIKLS0tLS1FTkQgUFVCTElDIEtFWS0tLS0tCg=='
111    _IMAGE_METADATA_SIGNATURE_WITH_KEY1='RH0QkJErsz9T6u5/0nNYKRjVPfH08+j/zdBU2BimU2jAa4zn7HElhIk4v7l92qtHJAdAfH8JzTCCUQcqxfBL0CLxoXClKbnFjb4DKKp5z4GHJ4Be7q5hq+OVFFTMs+WV6rVP+VPWfirKN3RQy2CaUnkcoaBsa9pgU3SfZYGK4EkgSY7Fwnjzeu1oCUb8v+VrY93Hia8vJgKRG1EBtK2a9qLy/2uZ9twHyKnykt5VeLXMUL7x2ChdY1IzJaPDfGMyneoUQ2k1Yr/ROuVaYWI08lZfM0W2Abj6uCCHNu/K2oPHevJs1yyy4lmRRr2aWDnZ93GA17Jb7XwJO4JgNUHQgQ=='
112    _IMAGE_PUBLIC_KEY2='LS0tLS1CRUdJTiBQVUJMSUMgS0VZLS0tLS0KTUlJQklqQU5CZ2txaGtpRzl3MEJBUUVGQUFPQ0FROEFNSUlCQ2dLQ0FRRUFxZE03Z25kNDNjV2ZRenlydDE2UQpESEUrVDB5eGcxOE9aTys5c2M4aldwakMxekZ0b01Gb2tFU2l1OVRMVXArS1VDMjc0ZitEeElnQWZTQ082VTVECkpGUlBYVXp2ZTF2YVhZZnFsalVCeGMrSlljR2RkNlBDVWw0QXA5ZjAyRGhrckduZi9ya0hPQ0VoRk5wbTUzZG8Kdlo5QTZRNUtCZmNnMUhlUTA4OG9wVmNlUUd0VW1MK2JPTnE1dEx2TkZMVVUwUnUwQW00QURKOFhtdzRycHZxdgptWEphRm1WdWYvR3g3K1RPbmFKdlpUZU9POUFKSzZxNlY4RTcrWlppTUljNUY0RU9zNUFYL2xaZk5PM1JWZ0cyCk83RGh6emErbk96SjNaSkdLNVI0V3daZHVobjlRUllvZ1lQQjBjNjI4NzhxWHBmMkJuM05wVVBpOENmL1JMTU0KbVFJREFRQUIKLS0tLS1FTkQgUFVCTElDIEtFWS0tLS0tCg=='
113    _IMAGE_METADATA_SIGNATURE_WITH_KEY2='TWzj+aki+yS0nKCzy0/t+pTdHMh0N9ffjNbwIJhSx9bQ3813oYaF3vQsky29o0PuDWih1363uEHOoCjYrzxLnwuk1NK7/6Psb88kAYjc7gk1IgW/xuFTybxSL0bbOAN4p1QI6oRMPYrf5mZCE+VHhbSaMazK0AebFEg0fwvXkaoo/pQQWRWCH0u0GCqDBfGZlQATe/79inXda4mD19E9EvvMqm2ZStys7MMPCkkYyEsZWFmm6AFykiWtVMNyc6JWSTKh6ZKsme+l2Rr5orTubFbdwMuItcb6KySnsgOj5MAQ+HVFfjgJuEs4eRsP/Tfjn213v4bJVUSt6DwABg7/gw=='
114
115    @staticmethod
116    def _string_has_strings(haystack, needles):
117        """Returns True iff all the strings in the list |needles| are
118        present in the string |haystack|."""
119        for n in needles:
120            if haystack.find(n) == -1:
121                return False
122        return True
123
124    def _check_signature(self, metadata_signature, public_key,
125                         expected_log_messages, failure_message):
126        """Helper function for updating with a Canned Omaha response."""
127
128        # Runs the update on the DUT and expect it to fail.
129        client_host = autotest.Autotest(self._host)
130        client_host.run_test(
131                'autoupdate_CannedOmahaUpdate',
132                image_url=self._staged_payload_url,
133                image_size=self._IMAGE_SIZE,
134                image_sha256=self._IMAGE_SHA256,
135                allow_failure=True,
136                metadata_size=self._IMAGE_METADATA_SIZE,
137                metadata_signature=metadata_signature,
138                public_key=public_key)
139
140        cmdresult = self._host.run('cat /var/log/update_engine.log')
141        if not self._string_has_strings(cmdresult.stdout,
142                                        expected_log_messages):
143            raise error.TestFail(failure_message)
144
145
146    def _check_bad_metadata_signature(self):
147        """Checks that update_engine rejects updates where the payload
148        and Omaha response do not agree on the metadata signature."""
149
150        expected_log_messages = [
151                'Mandating payload hash checks since Omaha Response for '
152                'unofficial build includes public RSA key',
153                'Mandatory metadata signature validation failed']
154
155        self._check_signature(
156                metadata_signature=self._IMAGE_METADATA_SIGNATURE_WITH_KEY1,
157                public_key=self._IMAGE_PUBLIC_KEY2,
158                expected_log_messages=expected_log_messages,
159                failure_message='Check for bad metadata signature failed.')
160
161
162    def _check_bad_payload_signature(self):
163        """Checks that update_engine rejects updates where the payload
164        signature does not match what is expected."""
165
166        expected_log_messages = [
167                'Mandating payload hash checks since Omaha Response for '
168                'unofficial build includes public RSA key',
169                'Metadata hash signature matches value in Omaha response.',
170                'Public key verification failed, thus update failed']
171
172        self._check_signature(
173                metadata_signature=self._IMAGE_METADATA_SIGNATURE_WITH_KEY2,
174                public_key=self._IMAGE_PUBLIC_KEY2,
175                expected_log_messages=expected_log_messages,
176                failure_message='Check for payload signature failed.')
177
178
179    def _stage_image(self, image_url):
180        """Requests an image server from the lab to stage the image
181        specified by |image_url| (typically a Google Storage
182        URL). Returns the URL to the staged image."""
183
184        # We don't have a build so just fake the string.
185        build = 'x86-fake-release/R42-4242.0.0-a1-bFAKE'
186        image_server = dev_server.ImageServer.resolve(build,
187                                                      self._host.hostname)
188        archive_url = os.path.dirname(image_url)
189        filename = os.path.basename(image_url)
190        # ImageServer expects an image parameter, but we don't have one.
191        image_server.stage_artifacts(image='fake_image',
192                                     files=[filename],
193                                     archive_url=archive_url)
194
195        # ImageServer has no way to give us the URL of the staged file...
196        base, name = _split_url(image_url)
197        staged_url = '%s/static/%s' % (image_server.url(), name)
198        return staged_url
199
200
201    def cleanup(self):
202        if self._host:
203            self._host.reboot()
204
205
206    def run_once(self, host):
207        """Runs the test on the DUT represented by |host|."""
208
209        self._host = host
210
211        # First, stage the image.
212        self._staged_payload_url = self._stage_image(self._IMAGE_GS_URL)
213
214        # Then run the tests.
215        self._check_bad_metadata_signature()
216        self._check_bad_payload_signature()
217
218