1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
|
# TPM Root Unlock on Halley2
Before this migration, the root token requires the approved PCR 7 pcrlock
policy. The new enrollment also requires an RSA signed PCR 11 policy at
`enter-initrd`. The `luks-root` policy reference is for root unlock; the
existing `initrd` reference remains for NvPCR setup. The root volume is
`/dev/nvme0n1p2`. Keep the tested disk passphrase and pcrlock recovery PIN
available away from this laptop.
The signing key, pcrlock policy, and LUKS metadata are host state. Do not put
them in this repo. Each kernel update must build and Secure Boot sign a UKI
with both PCR 11 signatures. A signed policy approves older UKIs that use the
same signing key and `luks-root` reference; it does not prevent rollback to
an older approved version.
## Prepare
Run these steps on Halley2, outside aibox. Stop if a check fails. No step in
this section changes the LUKS header.
```sh
pacman -Q systemd systemd-ukify mkinitcpio
sudo cryptsetup open --test-passphrase --key-slot 0 /dev/nvme0n1p2
sudo /usr/lib/systemd/systemd-pcrlock --strict=yes --pcr=7 --location=770 predict
df -h /boot
just apply
sudo mkinitcpio -P
sudo sbctl verify
just tpm-nvpcr-check
```
`systemd` and `systemd-ukify` must have the same version. The normal
`mkinitcpio -P` build replaces all four maintained UKIs. The checker verifies
their Secure Boot signatures, initrd services, and both PCR 11 signatures
against the measured contents of each image. It does not claim that the LUKS
token has changed. Before enrollment, check that the initrd has no separate
root key file or credential that could bypass the TPM test.
Back up the header and old TPM token in the encrypted root file system. These
files must stay root owned and must not go to the ESP or the repo. Check the
current LUKS metadata before using the token and slot numbers below. On this
host the existing TPM token is 0, its keyslot is 1, and the passphrase uses
slot 0. Stop if these facts have changed.
```sh
sudo cryptsetup luksDump --dump-json-metadata /dev/nvme0n1p2 | jq '{keyslots: (.keyslots|keys), tokens: (.tokens|with_entries(.value |= {type, keyslots, "tpm2-pcrs", tpm2_pcrlock, tpm2_pubkey_pcrs, tpm2_pubkey_ref}))}'
sudo install -d -m 0700 /var/lib/dotfiles/tpm-root
sudo cryptsetup luksHeaderBackup /dev/nvme0n1p2 \
--header-backup-file /var/lib/dotfiles/tpm-root/before.header
sudo install -m 0600 /dev/null /var/lib/dotfiles/tpm-root/old-token.json
sudo cryptsetup token export --token-id 0 \
--json-file /var/lib/dotfiles/tpm-root/old-token.json /dev/nvme0n1p2
```
## Enroll
Enroll a new keyslot with two required TPM policy shards. Do not request a
direct PCR value or a TPM PIN. Enrollment on the running system cannot test
the `enter-initrd` signature because PCR 11 has changed since that phase.
```sh
sudo systemd-cryptenroll /dev/nvme0n1p2 \
--unlock-tpm2-device=auto --tpm2-device=auto --tpm2-pcrs= \
--tpm2-pcrlock=/var/lib/systemd/pcrlock.json \
--tpm2-public-key=/etc/systemd/tpm2-pcr-public-key.pem \
--tpm2-public-key-pcrs=11 \
--tpm2-public-key-policyref=luks-root --tpm2-with-pin=no
sudo cryptsetup luksDump --dump-json-metadata /dev/nvme0n1p2 | jq '{keyslots: (.keyslots|keys), tokens: (.tokens|with_entries(.value |= {type, keyslots, "tpm2-pcrs", "tpm2-pcr-bank", tpm2_pcrlock, tpm2_pubkey_pcrs, tpm2_pubkey_ref}))}'
```
Confirm that the new TPM token has a different token ID and keyslot, and
requires `tpm2_pcrlock`, `tpm2_pubkey_pcrs: [11]`, and
`tpm2_pubkey_ref: "luks-root"`. Keep slot 0. If there is no new token, stop.
Remove only the old token metadata. Its keyslot remains for reversible
rollback until the boot tests pass. The new token is now the only automatic
unlock path. This command assumes the old token is still ID 0.
```sh
sudo cryptsetup token remove --token-id 0 /dev/nvme0n1p2
just tpm-unlock-check --trial
```
## Test
Boot the normal Hardened entry once. Confirm automatic unlock, then run:
```sh
efibootmgr | rg 'BootCurrent|BootOrder'
just tpm-nvpcr-check
just tpm-unlock-check --trial
systemctl --failed --no-pager
```
The image check verifies all four maintained UKIs without booting each one.
The token check verifies header metadata. The main boot proves that the
combined token can unlock the root volume.
For a refusal test, make a copy of the main UKI without the `luks-root`
approval. The helper keeps the `initrd` approval and the measured sections.
Re-sign the modified image for Secure Boot, then check the new signature. The
unsigned intermediate file remains on the encrypted root file system.
```sh
sudo python3 -m scripts.tpm_negative_uki \
/boot/EFI/Linux/arch-linux-hardened.efi \
/var/lib/dotfiles/tpm-root/no-root-approval.unsigned.efi
sudo sbsign --key /var/lib/sbctl/keys/db/db.key \
--cert /var/lib/sbctl/keys/db/db.pem \
--output /boot/EFI/Linux/arch-linux-hardened-no-root-approval.efi \
/var/lib/dotfiles/tpm-root/no-root-approval.unsigned.efi
sudo sbverify --cert /var/lib/sbctl/keys/db/db.pem \
/boot/EFI/Linux/arch-linux-hardened-no-root-approval.efi
sudo efibootmgr --create-only --disk /dev/nvme0n1 --part 1 \
--label 'Arch Root Policy Refusal Test' \
--loader '\EFI\Linux\arch-linux-hardened-no-root-approval.efi'
sudo efibootmgr
```
Read the new EFI entry number. Set it for one boot with
`sudo efibootmgr --bootnext NNNN`. The test must request the disk passphrase.
Enter it and check the boot journal for the TPM policy failure. If it unlocks
automatically, stop: another automatic path still exists. The normal boot
order must stay unchanged.
After the refusal test, remove only that test entry and UKI, using its actual
entry number. Boot the normal Hardened entry again if needed. Keep the
passphrase available.
```sh
sudo efibootmgr -b NNNN -B
sudo rm -- /boot/EFI/Linux/arch-linux-hardened-no-root-approval.efi
sudo rm -- /var/lib/dotfiles/tpm-root/no-root-approval.unsigned.efi
just tpm-nvpcr-check
just tpm-unlock-check --trial
```
## Finish
When both tests pass, remove only the old, unbound TPM keyslot. This host used
slot 1. Confirm the slot number again before running the command. Do not use a
type-wide `--wipe-slot=tpm2` operation.
```sh
sudo cryptsetup luksDump /dev/nvme0n1p2
sudo cryptsetup luksKillSlot /dev/nvme0n1p2 1
just tpm-unlock-check
sudo cryptsetup open --test-passphrase --key-slot 0 /dev/nvme0n1p2
sudo cryptsetup luksHeaderBackup /dev/nvme0n1p2 \
--header-backup-file /var/lib/dotfiles/tpm-root/final.header
sudo mkinitcpio -P
just tpm-nvpcr-check
```
On a later approved reboot, confirm normal automatic unlock. Keep the final
header backup and the passphrase. Remove the pre-migration backup and token
export only after that boot succeeds.
## Recovery
If automatic unlock fails, use the disk passphrase. During the trial, restore
the old token if needed, after checking that token ID 0 is free. This restores
the previous, weaker automatic unlock policy. The old keyslot must still exist.
```sh
sudo cryptsetup token import --token-id 0 \
--json-file /var/lib/dotfiles/tpm-root/old-token.json /dev/nvme0n1p2
```
If the LUKS header is damaged, use the saved header and passphrase from an
external recovery system. Header restore replaces all current token and
keyslot changes; do not use it for an ordinary policy failure. Do not clear
the TPM or its NV indices.
For an intended Secure Boot change, review the PCR 7 prediction before
updating its policy. See [systemd-pcrlock](https://man.archlinux.org/man/systemd-pcrlock.8.en),
[systemd-cryptenroll](https://man.archlinux.org/man/systemd-cryptenroll.1.en),
and [cryptsetup-token](https://man.archlinux.org/man/cryptsetup-token.8.en).
## Add the GPT partition policy
The current root token already requires the pcrlock policy and signed PCR 11.
This change adds PCR 5 to the existing pcrlock policy. PCR 5 covers the disk
partition table. It does not bind file contents, the whole disk, or a kernel
version. Do not enroll a new LUKS token or remove the disk passphrase.
Run this once on Halley2, outside aibox. Keep the disk passphrase and pcrlock
recovery PIN available. Stop if a check fails. Confirm that `/dev/nvme0n1` is
the disk that contains the root volume. Do not reboot during these steps.
```sh
sudo cryptsetup open --test-passphrase --key-slot 0 /dev/nvme0n1p2
sudo /usr/lib/systemd/systemd-pcrlock --strict=yes --pcr=7 --location=770 predict
sudo test ! -e /var/lib/pcrlock.d/600-gpt.pcrlock.d/generated.pcrlock
sudo test ! -e /var/lib/dotfiles/tpm-gpt/pcr7-policy.json
sudo install -d -m 0700 /var/lib/dotfiles/tpm-gpt
sudo cp /var/lib/systemd/pcrlock.json /var/lib/dotfiles/tpm-gpt/pcr7-policy.json
sudo cp /etc/systemd/system/systemd-pcrlock-make-policy.service.d/pcr7.conf \
/var/lib/dotfiles/tpm-gpt/pcr7.conf
sudo /usr/lib/systemd/systemd-pcrlock lock-gpt /dev/nvme0n1
sudo /usr/lib/systemd/systemd-pcrlock --strict=yes --pcr=5 --pcr=7 \
--location=770 predict
just apply
sudo /usr/lib/systemd/systemd-pcrlock make-policy \
--strict=yes --pcr=5 --pcr=7 --location=770
sudo /usr/bin/python3 - <<'PY'
import json
from pathlib import Path
old = json.loads(Path('/var/lib/dotfiles/tpm-gpt/pcr7-policy.json').read_text())
current = json.loads(Path('/var/lib/systemd/pcrlock.json').read_text())
if (old['nvIndex'], old['nvHandle']) != (current['nvIndex'], current['nvHandle']):
raise SystemExit('TPM NV index changed; stop')
print('verified: existing TPM NV index retained')
PY
just tpm-nvpcr-check
just tpm-unlock-check
```
The GPT command writes
`/var/lib/pcrlock.d/600-gpt.pcrlock.d/generated.pcrlock`. Stop and inspect
that file if it already exists. `just apply` installs the service override.
The Python check confirms that the existing TPM NV index is still in use.
The new checker requires both PCRs in the stored policy, a matching EFI boot
credential, and a strict prediction. Keep the backup on encrypted root storage.
The first normal reboot is the test of automatic unlock; do it only when you
are ready to enter the disk passphrase if TPM unlock fails. After that boot,
run both checks again and check `systemctl --failed`.
If a planned partition change makes PCR 5 differ, use the disk passphrase to
boot. Review the new layout before you approve it. Then run `lock-gpt` again,
run the strict PCR 5+7 prediction, and update the policy with `make-policy`.
The pcrlock recovery PIN can authorize the update when the old policy no
longer matches. Do not approve an unexpected partition change.
If the new policy blocks automatic unlock, use the disk passphrase. To return
to the saved PCR 7 policy, restore the old service override and update the
TPM policy. `--force` also rewrites the EFI credential when the prediction
equals an earlier policy. Do not copy the old JSON directly over the active
policy: the TPM NV index must be updated too.
```sh
sudo install -m 0644 /var/lib/dotfiles/tpm-gpt/pcr7.conf \
/etc/systemd/system/systemd-pcrlock-make-policy.service.d/pcr7.conf
sudo systemctl daemon-reload
sudo /usr/lib/systemd/systemd-pcrlock make-policy --force \
--recovery-pin=query --strict=yes --pcr=7 --location=770
sudo /usr/lib/systemd/systemd-pcrlock --strict=yes --pcr=7 --location=770 predict
```
The repo checker will then fail until you restore the PCR 5 setup. Do not
delete the saved policy or recovery PIN. The EFI boot test and root token
checks remain necessary because an offline prediction cannot prove unlock.
|