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('cmd_tpm_v2')
190def test_tpm2_get_capability(u_boot_console):
191    """Execute a TPM_GetCapability command.
192
193    Display one capability. In our test case, let's display the default DAM
194    lockout counter that should be 0 since the CLEAR:
195    - TPM_CAP_TPM_PROPERTIES = 0x6
196    - TPM_PT_LOCKOUT_COUNTER (1st parameter) = PTR_VAR + 14
197
198    There is no expected default values because it would depend on the chip
199    used. We can still save them in order to check they have changed later.
200    """
201    if is_sandbox(u_boot_console):
202        tpm2_sandbox_init(u_boot_console)
203
204    force_init(u_boot_console)
205    ram = u_boot_utils.find_ram_base(u_boot_console)
206
207    read_cap = u_boot_console.run_command('tpm2 get_capability 0x6 0x20e 0x200 1') #0x%x 1' % ram)
208    output = u_boot_console.run_command('echo $?')
209    assert output.endswith('0')
210    assert 'Property 0x0000020e: 0x00000000' in read_cap
211
212@pytest.mark.buildconfigspec('cmd_tpm_v2')
213def test_tpm2_dam_parameters(u_boot_console):
214    """Execute a TPM2_DictionaryAttackParameters command.
215
216    Change Dictionary Attack Mitigation (DAM) parameters. Ask the TPM to change:
217    - Max number of failed authentication before lockout: 3
218    - Time before the failure counter is automatically decremented: 10 sec
219    - Time after a lockout failure before it can be attempted again: 0 sec
220
221    For an unknown reason, the DAM parameters must be changed before changing
222    the authentication, otherwise the lockout will be engaged after the first
223    failed authentication attempt.
224    """
225    if is_sandbox(u_boot_console):
226        tpm2_sandbox_init(u_boot_console)
227    force_init(u_boot_console)
228    ram = u_boot_utils.find_ram_base(u_boot_console)
229
230    # Set the DAM parameters to known values
231    u_boot_console.run_command('tpm2 dam_parameters 3 10 0')
232    output = u_boot_console.run_command('echo $?')
233    assert output.endswith('0')
234
235    # Check the values have been saved
236    read_cap = u_boot_console.run_command('tpm2 get_capability 0x6 0x20f 0x%x 3' % ram)
237    output = u_boot_console.run_command('echo $?')
238    assert output.endswith('0')
239    assert 'Property 0x0000020f: 0x00000003' in read_cap
240    assert 'Property 0x00000210: 0x0000000a' in read_cap
241    assert 'Property 0x00000211: 0x00000000' in read_cap
242
243@pytest.mark.buildconfigspec('cmd_tpm_v2')
244def test_tpm2_pcr_read(u_boot_console):
245    """Execute a TPM2_PCR_Read command.
246
247    Perform a PCR read of the 0th PCR. Must be zero.
248    """
249    if is_sandbox(u_boot_console):
250        tpm2_sandbox_init(u_boot_console)
251
252    force_init(u_boot_console)
253    ram = u_boot_utils.find_ram_base(u_boot_console)
254
255    read_pcr = u_boot_console.run_command('tpm2 pcr_read 0 0x%x' % ram)
256    output = u_boot_console.run_command('echo $?')
257    assert output.endswith('0')
258
259    # Save the number of PCR updates
260    str = re.findall(r'\d+ known updates', read_pcr)[0]
261    global updates
262    updates = int(re.findall(r'\d+', str)[0])
263
264    # Check the output value
265    assert 'PCR #0 content' in read_pcr
266    assert '00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00' in read_pcr
267
268@pytest.mark.buildconfigspec('cmd_tpm_v2')
269def test_tpm2_pcr_extend(u_boot_console):
270    """Execute a TPM2_PCR_Extend command.
271
272    Perform a PCR extension with a known hash in memory (zeroed since the board
273    must have been rebooted).
274
275    No authentication mechanism is used here, not protecting against packet
276    replay, yet.
277    """
278    if is_sandbox(u_boot_console):
279        tpm2_sandbox_init(u_boot_console)
280    force_init(u_boot_console)
281    ram = u_boot_utils.find_ram_base(u_boot_console)
282
283    u_boot_console.run_command('tpm2 pcr_extend 0 0x%x' % ram)
284    output = u_boot_console.run_command('echo $?')
285    assert output.endswith('0')
286
287    # Read the value back into a different place so we can still use 'ram' as
288    # our zero bytes
289    read_pcr = u_boot_console.run_command('tpm2 pcr_read 0 0x%x' % (ram + 0x20))
290    output = u_boot_console.run_command('echo $?')
291    assert output.endswith('0')
292    assert 'f5 a5 fd 42 d1 6a 20 30 27 98 ef 6e d3 09 97 9b' in read_pcr
293    assert '43 00 3d 23 20 d9 f0 e8 ea 98 31 a9 27 59 fb 4b' in read_pcr
294
295    str = re.findall(r'\d+ known updates', read_pcr)[0]
296    new_updates = int(re.findall(r'\d+', str)[0])
297    assert (updates + 1) == new_updates
298
299    u_boot_console.run_command('tpm2 pcr_extend 0 0x%x' % ram)
300    output = u_boot_console.run_command('echo $?')
301    assert output.endswith('0')
302
303    read_pcr = u_boot_console.run_command('tpm2 pcr_read 0 0x%x' % (ram + 0x20))
304    output = u_boot_console.run_command('echo $?')
305    assert output.endswith('0')
306    assert '7a 05 01 f5 95 7b df 9c b3 a8 ff 49 66 f0 22 65' in read_pcr
307    assert 'f9 68 65 8b 7a 9c 62 64 2c ba 11 65 e8 66 42 f5' in read_pcr
308
309    str = re.findall(r'\d+ known updates', read_pcr)[0]
310    new_updates = int(re.findall(r'\d+', str)[0])
311    assert (updates + 2) == new_updates
312
313@pytest.mark.buildconfigspec('cmd_tpm_v2')
314def test_tpm2_cleanup(u_boot_console):
315    """Ensure the TPM is cleared from password or test related configuration."""
316
317    force_init(u_boot_console, True)
318