1# SPDX-License-Identifier: GPL-2.0+
2# Copyright (c) 2018, Bootlin
3# Author: Miquel Raynal <miquel.raynal@bootlin.com>
4
5import os.path
6import pytest
7import utils
8import re
9import time
10
11"""
12Test the TPMv2.x related commands. You must have a working hardware setup in
13order to do these tests.
14
15Notes:
16* These tests will prove the password mechanism. The TPM chip must be cleared of
17any password.
18* Commands like pcr_setauthpolicy and pcr_resetauthpolicy are not implemented
19here because they would fail the tests in most cases (TPMs do not implement them
20and return an error).
21
22
23Note:
24This test doesn't rely on boardenv_* configuration value but can change test
25behavior.
26
27* Setup env__tpm_device_test_skip to True if tests with TPM devices should be
28skipped.
29
30Parallel tests
31--------------
32
33These tests can be run in parallel on sandbox. In that case any action taken
34by one test may be independent of another. For sandbox, care should be taken to
35ensure that tests are independent.
36
37Unfortunately, tests cannot be made independent on real hardware, since there is
38no way to reset the TPM other than restarting the board. Perhaps that would be
39the best approach?
40"""
41
42updates = 0
43
44def force_init(ubman, force=False):
45    """When a test fails, U-Boot is reset. Because TPM stack must be initialized
46    after each reboot, we must ensure these lines are always executed before
47    trying any command or they will fail with no reason. Executing 'tpm init'
48    twice will spawn an error used to detect that the TPM was not reset and no
49    initialization code should be run.
50    """
51    skip_test = ubman.config.env.get('env__tpm_device_test_skip', False)
52    if skip_test:
53        pytest.skip('skip TPM device test')
54    output = ubman.run_command('tpm2 autostart')
55    if force or not 'Error' in output:
56        ubman.run_command('echo --- start of init ---')
57        ubman.run_command('tpm2 clear TPM2_RH_LOCKOUT')
58        output = ubman.run_command('echo $?')
59        if not output.endswith('0'):
60            ubman.run_command('tpm2 clear TPM2_RH_PLATFORM')
61        ubman.run_command('echo --- end of init ---')
62
63@pytest.mark.buildconfigspec('cmd_tpm_v2')
64def test_tpm2_autostart(ubman):
65    """Init the software stack to use TPMv2 commands."""
66    skip_test = ubman.config.env.get('env__tpm_device_test_skip', False)
67    if skip_test:
68        pytest.skip('skip TPM device test')
69    ubman.run_command('tpm2 autostart')
70    output = ubman.run_command('echo $?')
71    assert output.endswith('0')
72
73@pytest.mark.buildconfigspec('cmd_tpm_v2')
74def test_tpm2_continue_self_test(ubman):
75    """Execute a TPM2_SelfTest (continued) command.
76
77    Ask the TPM to finish its self tests (alternative to the full test) in order
78    to enter a fully operational state.
79    """
80
81    skip_test = ubman.config.env.get('env__tpm_device_test_skip', False)
82    if skip_test:
83        pytest.skip('skip TPM device test')
84    ubman.run_command('tpm2 self_test continue')
85    output = ubman.run_command('echo $?')
86    assert output.endswith('0')
87
88@pytest.mark.buildconfigspec('cmd_tpm_v2')
89def test_tpm2_clear(ubman):
90    """Execute a TPM2_Clear command.
91
92    Ask the TPM to reset entirely its internal state (including internal
93    configuration, passwords, counters and DAM parameters). This is half of the
94    TAKE_OWNERSHIP command from TPMv1.
95
96    Use the LOCKOUT hierarchy for this. The LOCKOUT/PLATFORM hierarchies must
97    not have a password set, otherwise this test will fail. ENDORSEMENT and
98    PLATFORM hierarchies are also available.
99    """
100    skip_test = ubman.config.env.get('env__tpm_device_test_skip', False)
101    if skip_test:
102        pytest.skip('skip TPM device test')
103    ubman.run_command('tpm2 clear TPM2_RH_LOCKOUT')
104    output = ubman.run_command('echo $?')
105    assert output.endswith('0')
106
107    ubman.run_command('tpm2 clear TPM2_RH_PLATFORM')
108    output = ubman.run_command('echo $?')
109    assert output.endswith('0')
110
111@pytest.mark.buildconfigspec('cmd_tpm_v2')
112def test_tpm2_change_auth(ubman):
113    """Execute a TPM2_HierarchyChangeAuth command.
114
115    Ask the TPM to change the owner, ie. set a new password: 'unicorn'
116
117    Use the LOCKOUT hierarchy for this. ENDORSEMENT and PLATFORM hierarchies are
118    also available.
119    """
120    force_init(ubman)
121
122    ubman.run_command('tpm2 change_auth TPM2_RH_LOCKOUT unicorn')
123    output = ubman.run_command('echo $?')
124    assert output.endswith('0')
125
126    ubman.run_command('tpm2 clear TPM2_RH_LOCKOUT unicorn')
127    output = ubman.run_command('echo $?')
128    ubman.run_command('tpm2 clear TPM2_RH_PLATFORM')
129    assert output.endswith('0')
130
131@pytest.mark.buildconfigspec('sandbox')
132@pytest.mark.buildconfigspec('cmd_tpm_v2')
133def test_tpm2_get_capability(ubman):
134    """Execute a TPM_GetCapability command.
135
136    Display one capability. In our test case, let's display the default DAM
137    lockout counter that should be 0 since the CLEAR:
138    - TPM_CAP_TPM_PROPERTIES = 0x6
139    - TPM_PT_LOCKOUT_COUNTER (1st parameter) = PTR_VAR + 14
140
141    There is no expected default values because it would depend on the chip
142    used. We can still save them in order to check they have changed later.
143    """
144    force_init(ubman)
145    ram = utils.find_ram_base(ubman)
146
147    read_cap = ubman.run_command('tpm2 get_capability 0x6 0x20e 0x200 1') #0x%x 1' % ram)
148    output = ubman.run_command('echo $?')
149    assert output.endswith('0')
150    assert 'Property 0x0000020e: 0x00000000' in read_cap
151
152@pytest.mark.buildconfigspec('cmd_tpm_v2')
153def test_tpm2_dam_parameters(ubman):
154    """Execute a TPM2_DictionaryAttackParameters command.
155
156    Change Dictionary Attack Mitigation (DAM) parameters. Ask the TPM to change:
157    - Max number of failed authentication before lockout: 3
158    - Time before the failure counter is automatically decremented: 10 sec
159    - Time after a lockout failure before it can be attempted again: 0 sec
160
161    For an unknown reason, the DAM parameters must be changed before changing
162    the authentication, otherwise the lockout will be engaged after the first
163    failed authentication attempt.
164    """
165    force_init(ubman)
166    ram = utils.find_ram_base(ubman)
167
168    # Set the DAM parameters to known values
169    ubman.run_command('tpm2 dam_parameters 3 10 0')
170    output = ubman.run_command('echo $?')
171    assert output.endswith('0')
172
173    # Check the values have been saved
174    read_cap = ubman.run_command('tpm2 get_capability 0x6 0x20f 0x%x 3' % ram)
175    output = ubman.run_command('echo $?')
176    assert output.endswith('0')
177    assert 'Property 0x0000020f: 0x00000003' in read_cap
178    assert 'Property 0x00000210: 0x0000000a' in read_cap
179    assert 'Property 0x00000211: 0x00000000' in read_cap
180
181@pytest.mark.buildconfigspec('cmd_tpm_v2')
182@pytest.mark.notbuildconfigspec('target_chromebook_coral')
183def test_tpm2_pcr_read(ubman):
184    """Execute a TPM2_PCR_Read command.
185
186    Perform a PCR read of the 10th PCR. Must be zero.
187    """
188    force_init(ubman)
189    ram = utils.find_ram_base(ubman)
190
191    read_pcr = ubman.run_command('tpm2 pcr_read 10 0x%x' % ram)
192    output = ubman.run_command('echo $?')
193    assert output.endswith('0')
194
195    # Save the number of PCR updates
196    str = re.findall(r'\d+ known updates', read_pcr)[0]
197    global updates
198    updates = int(re.findall(r'\d+', str)[0])
199
200    # Check the output value
201    assert 'PCR #10 sha256 32 byte content' in read_pcr
202    assert '00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00' in read_pcr
203
204@pytest.mark.buildconfigspec('cmd_tpm_v2')
205@pytest.mark.notbuildconfigspec('target_chromebook_coral')
206def test_tpm2_pcr_extend(ubman):
207    """Execute a TPM2_PCR_Extend command.
208
209    Perform a PCR extension with a known hash in memory (zeroed since the board
210    must have been rebooted).
211
212    No authentication mechanism is used here, not protecting against packet
213    replay, yet.
214    """
215    force_init(ubman)
216    ram = utils.find_ram_base(ubman)
217
218    read_pcr = ubman.run_command('tpm2 pcr_read 10 0x%x' % (ram + 0x20))
219    output = ubman.run_command('echo $?')
220    assert output.endswith('0')
221    str = re.findall(r'\d+ known updates', read_pcr)[0]
222    updates = int(re.findall(r'\d+', str)[0])
223
224    ubman.run_command('tpm2 pcr_extend 10 0x%x' % ram)
225    output = ubman.run_command('echo $?')
226    assert output.endswith('0')
227
228    # Read the value back into a different place so we can still use 'ram' as
229    # our zero bytes
230    read_pcr = ubman.run_command('tpm2 pcr_read 10 0x%x' % (ram + 0x20))
231    output = ubman.run_command('echo $?')
232    assert output.endswith('0')
233    assert 'f5 a5 fd 42 d1 6a 20 30 27 98 ef 6e d3 09 97 9b' in read_pcr
234    assert '43 00 3d 23 20 d9 f0 e8 ea 98 31 a9 27 59 fb 4b' in read_pcr
235
236    str = re.findall(r'\d+ known updates', read_pcr)[0]
237    new_updates = int(re.findall(r'\d+', str)[0])
238    assert (updates + 1) == new_updates
239
240    ubman.run_command('tpm2 pcr_extend 10 0x%x' % ram)
241    output = ubman.run_command('echo $?')
242    assert output.endswith('0')
243
244    read_pcr = ubman.run_command('tpm2 pcr_read 10 0x%x' % (ram + 0x20))
245    output = ubman.run_command('echo $?')
246    assert output.endswith('0')
247    assert '7a 05 01 f5 95 7b df 9c b3 a8 ff 49 66 f0 22 65' in read_pcr
248    assert 'f9 68 65 8b 7a 9c 62 64 2c ba 11 65 e8 66 42 f5' in read_pcr
249
250    str = re.findall(r'\d+ known updates', read_pcr)[0]
251    new_updates = int(re.findall(r'\d+', str)[0])
252    assert (updates + 2) == new_updates
253
254@pytest.mark.buildconfigspec('cmd_tpm_v2')
255def test_tpm2_cleanup(ubman):
256    """Ensure the TPM is cleared from password or test related configuration."""
257
258    force_init(ubman, True)
259