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/strc/platform/dev
46      $ ./devserver.py --test_image                               \
47                       --private_key                              \
48                         ../update_engine/unittest_key.pem        \
49                       --private_key_for_metadata_hash_signature  \
50                         ../update_engine/unittest_key.pem        \
51                       --public_key                               \
52                          ../update_engine/unittest_key2.pub.pem
53
54    3. Update the DUT - the update should fail at the metadata
55       verification stage.
56
57    4. From the update_engine logs (stored in /var/log/update_engine/)
58       on the DUT, find the Omaha response sent to the DUT and update
59       the following constants with values from the XML:
60
61        _IMAGE_SHA256: set it to the 'sha256'
62        _IMAGE_METADATA_SIZE: set it to the 'MetadataSize'
63        _IMAGE_PUBLIC_KEY2: set it to the 'PublicKeyRsa'
64        _IMAGE_METADATA_SIGNATURE_WITH_KEY1: set it to 'MetadataSignatureRsa'
65
66       Also download the image payload ('url' and 'codebase' tags),
67       upload it to Google Storage and update the _IMAGE_GS_URL and
68       _IMAGE_SIZE constants with the resulting URL and the size.
69
70    5. Serve the image to the DUT again and note the slightly different
71       parameters this time. Note that the image served is the same,
72       however the Omaha response will be different.
73
74      $ cd ~/trunk/strc/platform/dev
75      $ ./devserver.py --test_image                               \
76                       --private_key                              \
77                         ../update_engine/unittest_key.pem        \
78                       --private_key_for_metadata_hash_signature  \
79                         ../update_engine/unittest_key2.pem       \
80                       --public_key                               \
81                          ../update_engine/unittest_key2.pub.pem
82
83    6. Update the DUT - the update should fail at the payload
84       verification stage.
85
86    7. Like in step 4., examine the update_engine logs and update the
87       following constants:
88
89        _IMAGE_METADATA_SIGNATURE_WITH_KEY2: set to 'MetadataSignatureRsa'
90
91    8. Now run the test and ensure that it passes
92
93      $ cd ~/trunk/src/scripts
94      $ test_that -b ${BOARD} --fast <DUT_IP> autoupdate_CatchBadSignatures
95
96    """
97    version = 1
98
99    # The test image to use and the values associated with it.
100    _IMAGE_GS_URL='gs://chromiumos-test-assets-public/autoupdate/autoupdate_CatchBadSignatures-payload-lumpy-R33-4970.0.2013_11_15_1654-a1'
101    _IMAGE_SIZE=369080798
102    _IMAGE_METADATA_SIZE=58439
103    _IMAGE_SHA256='FeWxyPLdz1/UDHxskkYSkQm64D5kQgaSb1IEXX3/sjA='
104    _IMAGE_PUBLIC_KEY1='LS0tLS1CRUdJTiBQVUJMSUMgS0VZLS0tLS0KTUlJQklqQU5CZ2txaGtpRzl3MEJBUUVGQUFPQ0FROEFNSUlCQ2dLQ0FRRUF4NmhxUytIYmM3ak44MmVrK09CawpISk52bFdYa3R5UzJYQ3VRdEd0bkhqRVY3T3U1aEhORjk2czV3RW44UkR0cmRMb2NtMGErU3FYWGY3S3ljRlJUClp4TGREYnFXQU1VbFBYT2FQSStQWkxXa0I3L0tWN0NkajZ2UEdiZXE3ZWx1K2FUL2J1ZGh6VHZxMnN0WXJyQWwKY3IvMjF0T1ZEUGlXdGZDZHlraDdGQ1hpNkZhWUhvTnk1QTZFS1FMZkxCdUpvVS9Rb0N1ZmxkbXdsRmFGREtsKwpLb29zNlIxUVlKZkNOWmZnb2NyVzFQSWgrOHQxSkl2dzZJem84K2ZUbWU3bmV2N09sMllaaU1XSnBSWUt4OE1nCnhXMlVnVFhsUnBtUU41NnBjOUxVc25WQThGTkRCTjU3K2dNSmorWG1kRG1idE1wT3N5WGZTTkVnbnV3TVBjRWMKbXdJREFRQUIKLS0tLS1FTkQgUFVCTElDIEtFWS0tLS0tCg=='
105    _IMAGE_METADATA_SIGNATURE_WITH_KEY1='ZA3p2Gfh6qKNqf0as3LeoEou1AsfP73khfk0+hiJ0UFmqTTMj1b8PBeHSHzeRNoJvNZfZBD372PH0BSlKm4BeJ6qyySVDTyC55pKOQyQaC/c5tncvknId2acSEp0XSM5wvkXON0kS9sPfJi7qxDaTJnoCGi6gDKiMjEH3WhsE/1FG5AQ1HPibbeBK3RTtxGmqOIYses+RvJTag7wobdUnXe2Q7l6/c+wCD6m99yXK6l6Vm05gjAR7nhMd4ZyVfN2xaX8KSt9VybO3UuvG9yQUDhxy+ZURY0aaQPLdYcTsuocg/hqDlXctl6WBf6lKogeVqgfaypXqkPlYfgf0tHDGg=='
106    _IMAGE_PUBLIC_KEY2='LS0tLS1CRUdJTiBQVUJMSUMgS0VZLS0tLS0KTUlJQklqQU5CZ2txaGtpRzl3MEJBUUVGQUFPQ0FROEFNSUlCQ2dLQ0FRRUFxZE03Z25kNDNjV2ZRenlydDE2UQpESEUrVDB5eGcxOE9aTys5c2M4aldwakMxekZ0b01Gb2tFU2l1OVRMVXArS1VDMjc0ZitEeElnQWZTQ082VTVECkpGUlBYVXp2ZTF2YVhZZnFsalVCeGMrSlljR2RkNlBDVWw0QXA5ZjAyRGhrckduZi9ya0hPQ0VoRk5wbTUzZG8Kdlo5QTZRNUtCZmNnMUhlUTA4OG9wVmNlUUd0VW1MK2JPTnE1dEx2TkZMVVUwUnUwQW00QURKOFhtdzRycHZxdgptWEphRm1WdWYvR3g3K1RPbmFKdlpUZU9POUFKSzZxNlY4RTcrWlppTUljNUY0RU9zNUFYL2xaZk5PM1JWZ0cyCk83RGh6emErbk96SjNaSkdLNVI0V3daZHVobjlRUllvZ1lQQjBjNjI4NzhxWHBmMkJuM05wVVBpOENmL1JMTU0KbVFJREFRQUIKLS0tLS1FTkQgUFVCTElDIEtFWS0tLS0tCg=='
107    _IMAGE_METADATA_SIGNATURE_WITH_KEY2='k+mg0w5Jy8DXdF9Vw0MJdJdAj1S4EYR3k9fR4ECZmZplhmzUFvyPWVAHEYDEGLtNBbdaa66+ErOE/clERfxvjkIHbIlTUWDnqgKKPYnZ5dNuEDrHn8875ild9OwBgHEK7NSxaNyRGThLfVCqIUKUzMnjBk/elAiiY0hlLIN9Owitw3f+p9E2chYSdh1dpqlcs14JCULcO/+p+ZfQdeNkN600tS02SGOBwV4W8wXt7EWYdu2awp39z+zDniudFShIpamhhUbddqAn7aZTNE6qGgYVZuWNv3O3kBY4dMb7NsSZInn+fkC39QlXlqoM+ShLVhlpJa/MdOpX7g1UQKLa9A=='
108
109    @staticmethod
110    def _string_has_strings(haystack, needles):
111        """Returns True iff all the strings in the list |needles| are
112        present in the string |haystack|."""
113        for n in needles:
114            if haystack.find(n) == -1:
115                return False
116        return True
117
118    def _check_signature(self, metadata_signature, public_key,
119                         expected_log_messages, failure_message):
120        """Helper function for updating with a Canned Omaha response."""
121
122        # Runs the update on the DUT and expect it to fail.
123        client_host = autotest.Autotest(self._host)
124        client_host.run_test(
125                'autoupdate_CannedOmahaUpdate',
126                image_url=self._staged_payload_url,
127                image_size=self._IMAGE_SIZE,
128                image_sha256=self._IMAGE_SHA256,
129                allow_failure=True,
130                metadata_size=self._IMAGE_METADATA_SIZE,
131                metadata_signature=metadata_signature,
132                public_key=public_key)
133
134        cmdresult = self._host.run('cat /var/log/update_engine.log')
135        if not self._string_has_strings(cmdresult.stdout,
136                                        expected_log_messages):
137            raise error.TestFail(failure_message)
138
139
140    def _check_bad_metadata_signature(self):
141        """Checks that update_engine rejects updates where the payload
142        and Omaha response do not agree on the metadata signature."""
143
144        expected_log_messages = [
145                'Mandating payload hash checks since Omaha Response for '
146                'unofficial build includes public RSA key',
147                'Mandatory metadata signature validation failed']
148
149        self._check_signature(
150                metadata_signature=self._IMAGE_METADATA_SIGNATURE_WITH_KEY1,
151                public_key=self._IMAGE_PUBLIC_KEY2,
152                expected_log_messages=expected_log_messages,
153                failure_message='Check for bad metadata signature failed.')
154
155
156    def _check_bad_payload_signature(self):
157        """Checks that update_engine rejects updates where the payload
158        signature does not match what is expected."""
159
160        expected_log_messages = [
161                'Mandating payload hash checks since Omaha Response for '
162                'unofficial build includes public RSA key',
163                'Metadata hash signature matches value in Omaha response.',
164                'Public key verification failed, thus update failed']
165
166        self._check_signature(
167                metadata_signature=self._IMAGE_METADATA_SIGNATURE_WITH_KEY2,
168                public_key=self._IMAGE_PUBLIC_KEY2,
169                expected_log_messages=expected_log_messages,
170                failure_message='Check for payload signature failed.')
171
172
173    def _stage_image(self, image_url):
174        """Requests an image server from the lab to stage the image
175        specified by |image_url| (typically a Google Storage
176        URL). Returns the URL to the staged image."""
177
178        # We don't have a build so just fake the string.
179        build = 'x86-fake-release/R42-4242.0.0-a1-bFAKE'
180        image_server = dev_server.ImageServer.resolve(build)
181        archive_url = os.path.dirname(image_url)
182        filename = os.path.basename(image_url)
183        # ImageServer expects an image parameter, but we don't have one.
184        image_server.stage_artifacts(image='fake_image',
185                                     files=[filename],
186                                     archive_url=archive_url)
187
188        # ImageServer has no way to give us the URL of the staged file...
189        base, name = _split_url(image_url)
190        staged_url = '%s/static/%s' % (image_server.url(), name)
191        return staged_url
192
193
194    def cleanup(self):
195        if self._host:
196            self._host.reboot()
197
198
199    def run_once(self, host):
200        """Runs the test on the DUT represented by |host|."""
201
202        self._host = host
203
204        # First, stage the image.
205        self._staged_payload_url = self._stage_image(self._IMAGE_GS_URL)
206
207        # Then run the tests.
208        self._check_bad_metadata_signature()
209        self._check_bad_payload_signature()
210
211