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 u_boot_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
30"""
31
32updates = 0
33
34def force_init(u_boot_console, force=False):
35    """When a test fails, U-Boot is reset. Because TPM stack must be initialized
36    after each reboot, we must ensure these lines are always executed before
37    trying any command or they will fail with no reason. Executing 'tpm init'
38    twice will spawn an error used to detect that the TPM was not reset and no
39    initialization code should be run.
40    """
41    skip_test = u_boot_console.config.env.get('env__tpm_device_test_skip', False)
42    if skip_test:
43        pytest.skip('skip TPM device test')
44    output = u_boot_console.run_command('tpm2 init')
45    if force or not 'Error' in output:
46        u_boot_console.run_command('echo --- start of init ---')
47        u_boot_console.run_command('tpm2 startup TPM2_SU_CLEAR')
48        u_boot_console.run_command('tpm2 self_test full')
49        u_boot_console.run_command('tpm2 clear TPM2_RH_LOCKOUT')
50        output = u_boot_console.run_command('echo $?')
51        if not output.endswith('0'):
52            u_boot_console.run_command('tpm2 clear TPM2_RH_PLATFORM')
53        u_boot_console.run_command('echo --- end of init ---')
54
55def is_sandbox(cons):
56    # Array slice removes leading/trailing quotes.
57    sys_arch = cons.config.buildconfig.get('config_sys_arch', '"sandbox"')[1:-1]
58    return sys_arch == 'sandbox'
59
60@pytest.mark.buildconfigspec('cmd_tpm_v2')
61def test_tpm2_init(u_boot_console):
62    """Init the software stack to use TPMv2 commands."""
63    skip_test = u_boot_console.config.env.get('env__tpm_device_test_skip', False)
64    if skip_test:
65        pytest.skip('skip TPM device test')
66    u_boot_console.run_command('tpm2 init')
67    output = u_boot_console.run_command('echo $?')
68    assert output.endswith('0')
69
70@pytest.mark.buildconfigspec('cmd_tpm_v2')
71def test_tpm2_startup(u_boot_console):
72    """Execute a TPM2_Startup command.
73
74    Initiate the TPM internal state machine.
75    """
76    u_boot_console.run_command('tpm2 startup TPM2_SU_CLEAR')
77    output = u_boot_console.run_command('echo $?')
78    assert output.endswith('0')
79
80def tpm2_sandbox_init(u_boot_console):
81    """Put sandbox back into a known state so we can run a test
82
83    This allows all tests to run in parallel, since no test depends on another.
84    """
85    u_boot_console.restart_uboot()
86    u_boot_console.run_command('tpm2 init')
87    output = u_boot_console.run_command('echo $?')
88    assert output.endswith('0')
89
90    skip_test = u_boot_console.config.env.get('env__tpm_device_test_skip', False)
91    if skip_test:
92        pytest.skip('skip TPM device test')
93    u_boot_console.run_command('tpm2 startup TPM2_SU_CLEAR')
94    output = u_boot_console.run_command('echo $?')
95    assert output.endswith('0')
96
97    u_boot_console.run_command('tpm2 self_test full')
98    output = u_boot_console.run_command('echo $?')
99    assert output.endswith('0')
100
101@pytest.mark.buildconfigspec('cmd_tpm_v2')
102def test_tpm2_sandbox_self_test_full(u_boot_console):
103    """Execute a TPM2_SelfTest (full) command.
104
105    Ask the TPM to perform all self tests to also enable full capabilities.
106    """
107    if is_sandbox(u_boot_console):
108        u_boot_console.restart_uboot()
109        u_boot_console.run_command('tpm2 init')
110        output = u_boot_console.run_command('echo $?')
111        assert output.endswith('0')
112
113        u_boot_console.run_command('tpm2 startup TPM2_SU_CLEAR')
114        output = u_boot_console.run_command('echo $?')
115        assert output.endswith('0')
116
117    skip_test = u_boot_console.config.env.get('env__tpm_device_test_skip', False)
118    if skip_test:
119        pytest.skip('skip TPM device test')
120    u_boot_console.run_command('tpm2 self_test full')
121    output = u_boot_console.run_command('echo $?')
122    assert output.endswith('0')
123
124@pytest.mark.buildconfigspec('cmd_tpm_v2')
125def test_tpm2_continue_self_test(u_boot_console):
126    """Execute a TPM2_SelfTest (continued) command.
127
128    Ask the TPM to finish its self tests (alternative to the full test) in order
129    to enter a fully operational state.
130    """
131
132    skip_test = u_boot_console.config.env.get('env__tpm_device_test_skip', False)
133    if skip_test:
134        pytest.skip('skip TPM device test')
135    if is_sandbox(u_boot_console):
136        tpm2_sandbox_init(u_boot_console)
137    u_boot_console.run_command('tpm2 self_test continue')
138    output = u_boot_console.run_command('echo $?')
139    assert output.endswith('0')
140
141@pytest.mark.buildconfigspec('cmd_tpm_v2')
142def test_tpm2_clear(u_boot_console):
143    """Execute a TPM2_Clear command.
144
145    Ask the TPM to reset entirely its internal state (including internal
146    configuration, passwords, counters and DAM parameters). This is half of the
147    TAKE_OWNERSHIP command from TPMv1.
148
149    Use the LOCKOUT hierarchy for this. The LOCKOUT/PLATFORM hierarchies must
150    not have a password set, otherwise this test will fail. ENDORSEMENT and
151    PLATFORM hierarchies are also available.
152    """
153    if is_sandbox(u_boot_console):
154        tpm2_sandbox_init(u_boot_console)
155
156    skip_test = u_boot_console.config.env.get('env__tpm_device_test_skip', False)
157    if skip_test:
158        pytest.skip('skip TPM device test')
159    u_boot_console.run_command('tpm2 clear TPM2_RH_LOCKOUT')
160    output = u_boot_console.run_command('echo $?')
161    assert output.endswith('0')
162
163    u_boot_console.run_command('tpm2 clear TPM2_RH_PLATFORM')
164    output = u_boot_console.run_command('echo $?')
165    assert output.endswith('0')
166
167@pytest.mark.buildconfigspec('cmd_tpm_v2')
168def test_tpm2_change_auth(u_boot_console):
169    """Execute a TPM2_HierarchyChangeAuth command.
170
171    Ask the TPM to change the owner, ie. set a new password: 'unicorn'
172
173    Use the LOCKOUT hierarchy for this. ENDORSEMENT and PLATFORM hierarchies are
174    also available.
175    """
176    if is_sandbox(u_boot_console):
177        tpm2_sandbox_init(u_boot_console)
178    force_init(u_boot_console)
179
180    u_boot_console.run_command('tpm2 change_auth TPM2_RH_LOCKOUT unicorn')
181    output = u_boot_console.run_command('echo $?')
182    assert output.endswith('0')
183
184    u_boot_console.run_command('tpm2 clear TPM2_RH_LOCKOUT unicorn')
185    output = u_boot_console.run_command('echo $?')
186    u_boot_console.run_command('tpm2 clear TPM2_RH_PLATFORM')
187    assert output.endswith('0')
188
189@pytest.mark.buildconfigspec('sandbox')
190@pytest.mark.buildconfigspec('cmd_tpm_v2')
191def test_tpm2_get_capability(u_boot_console):
192    """Execute a TPM_GetCapability command.
193
194    Display one capability. In our test case, let's display the default DAM
195    lockout counter that should be 0 since the CLEAR:
196    - TPM_CAP_TPM_PROPERTIES = 0x6
197    - TPM_PT_LOCKOUT_COUNTER (1st parameter) = PTR_VAR + 14
198
199    There is no expected default values because it would depend on the chip
200    used. We can still save them in order to check they have changed later.
201    """
202    if is_sandbox(u_boot_console):
203        tpm2_sandbox_init(u_boot_console)
204
205    force_init(u_boot_console)
206    ram = u_boot_utils.find_ram_base(u_boot_console)
207
208    read_cap = u_boot_console.run_command('tpm2 get_capability 0x6 0x20e 0x200 1') #0x%x 1' % ram)
209    output = u_boot_console.run_command('echo $?')
210    assert output.endswith('0')
211    assert 'Property 0x0000020e: 0x00000000' in read_cap
212
213@pytest.mark.buildconfigspec('cmd_tpm_v2')
214def test_tpm2_dam_parameters(u_boot_console):
215    """Execute a TPM2_DictionaryAttackParameters command.
216
217    Change Dictionary Attack Mitigation (DAM) parameters. Ask the TPM to change:
218    - Max number of failed authentication before lockout: 3
219    - Time before the failure counter is automatically decremented: 10 sec
220    - Time after a lockout failure before it can be attempted again: 0 sec
221
222    For an unknown reason, the DAM parameters must be changed before changing
223    the authentication, otherwise the lockout will be engaged after the first
224    failed authentication attempt.
225    """
226    if is_sandbox(u_boot_console):
227        tpm2_sandbox_init(u_boot_console)
228    force_init(u_boot_console)
229    ram = u_boot_utils.find_ram_base(u_boot_console)
230
231    # Set the DAM parameters to known values
232    u_boot_console.run_command('tpm2 dam_parameters 3 10 0')
233    output = u_boot_console.run_command('echo $?')
234    assert output.endswith('0')
235
236    # Check the values have been saved
237    read_cap = u_boot_console.run_command('tpm2 get_capability 0x6 0x20f 0x%x 3' % ram)
238    output = u_boot_console.run_command('echo $?')
239    assert output.endswith('0')
240    assert 'Property 0x0000020f: 0x00000003' in read_cap
241    assert 'Property 0x00000210: 0x0000000a' in read_cap
242    assert 'Property 0x00000211: 0x00000000' in read_cap
243
244@pytest.mark.buildconfigspec('cmd_tpm_v2')
245def test_tpm2_pcr_read(u_boot_console):
246    """Execute a TPM2_PCR_Read command.
247
248    Perform a PCR read of the 0th PCR. Must be zero.
249    """
250    if is_sandbox(u_boot_console):
251        tpm2_sandbox_init(u_boot_console)
252
253    force_init(u_boot_console)
254    ram = u_boot_utils.find_ram_base(u_boot_console)
255
256    read_pcr = u_boot_console.run_command('tpm2 pcr_read 0 0x%x' % ram)
257    output = u_boot_console.run_command('echo $?')
258    assert output.endswith('0')
259
260    # Save the number of PCR updates
261    str = re.findall(r'\d+ known updates', read_pcr)[0]
262    global updates
263    updates = int(re.findall(r'\d+', str)[0])
264
265    # Check the output value
266    assert 'PCR #0 content' in read_pcr
267    assert '00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00' in read_pcr
268
269@pytest.mark.buildconfigspec('cmd_tpm_v2')
270def test_tpm2_pcr_extend(u_boot_console):
271    """Execute a TPM2_PCR_Extend command.
272
273    Perform a PCR extension with a known hash in memory (zeroed since the board
274    must have been rebooted).
275
276    No authentication mechanism is used here, not protecting against packet
277    replay, yet.
278    """
279    if is_sandbox(u_boot_console):
280        tpm2_sandbox_init(u_boot_console)
281    force_init(u_boot_console)
282    ram = u_boot_utils.find_ram_base(u_boot_console)
283
284    u_boot_console.run_command('tpm2 pcr_extend 0 0x%x' % ram)
285    output = u_boot_console.run_command('echo $?')
286    assert output.endswith('0')
287
288    # Read the value back into a different place so we can still use 'ram' as
289    # our zero bytes
290    read_pcr = u_boot_console.run_command('tpm2 pcr_read 0 0x%x' % (ram + 0x20))
291    output = u_boot_console.run_command('echo $?')
292    assert output.endswith('0')
293    assert 'f5 a5 fd 42 d1 6a 20 30 27 98 ef 6e d3 09 97 9b' in read_pcr
294    assert '43 00 3d 23 20 d9 f0 e8 ea 98 31 a9 27 59 fb 4b' in read_pcr
295
296    str = re.findall(r'\d+ known updates', read_pcr)[0]
297    new_updates = int(re.findall(r'\d+', str)[0])
298    assert (updates + 1) == new_updates
299
300    u_boot_console.run_command('tpm2 pcr_extend 0 0x%x' % ram)
301    output = u_boot_console.run_command('echo $?')
302    assert output.endswith('0')
303
304    read_pcr = u_boot_console.run_command('tpm2 pcr_read 0 0x%x' % (ram + 0x20))
305    output = u_boot_console.run_command('echo $?')
306    assert output.endswith('0')
307    assert '7a 05 01 f5 95 7b df 9c b3 a8 ff 49 66 f0 22 65' in read_pcr
308    assert 'f9 68 65 8b 7a 9c 62 64 2c ba 11 65 e8 66 42 f5' in read_pcr
309
310    str = re.findall(r'\d+ known updates', read_pcr)[0]
311    new_updates = int(re.findall(r'\d+', str)[0])
312    assert (updates + 2) == new_updates
313
314@pytest.mark.buildconfigspec('cmd_tpm_v2')
315def test_tpm2_cleanup(u_boot_console):
316    """Ensure the TPM is cleared from password or test related configuration."""
317
318    force_init(u_boot_console, True)
319