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