Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,14 @@ commands:

jobs:

opal_tests:
executor: heads-docker
steps:
- checkout
- run:
name: TCG Opal functional fixture
command: ./tests/opal/test_heads_opal.sh

# ═══════════════════════════════════════════════════════════════════════════
# Glossary (see doc/circleci.md for full cache model)
# ═══════════════════════════════════════════════════════════════════════════
Expand Down Expand Up @@ -418,6 +426,9 @@ workflows:
- create_hashes:
name: create_hashes [cache keys]

- opal_tests:
name: TCG Opal functional fixture

# ── x86 blobs ───────────────────────────────────────────────────────
# Chain: create_hashes -> x86_blobs -> x86_musl_cross_make -> x86_coreboot
- x86_blobs:
Expand Down Expand Up @@ -738,6 +749,13 @@ workflows:
requires:
- EOL_t480-hotp-maximized [seed:coreboot-25.09]

- build:
name: qemu-coreboot-fbwhiptail-tpm2-opal
target: qemu-coreboot-fbwhiptail-tpm2-opal
subcommand: ""
requires:
- EOL_t480-hotp-maximized [seed:coreboot-25.09]

# ── purism boards (alphabetical) ──────────────────────────────────────
- build:
name: EOL_librem_13v2
Expand Down
30 changes: 30 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -777,6 +777,36 @@ bin_modules-$(CONFIG_ZSTD) += zstd
bin_modules-$(CONFIG_E2FSPROGS) += e2fsprogs
bin_modules-$(CONFIG_EXFATPROGS) += exfatprogs

ifeq "$(CONFIG_HEADS_OPAL)" "y"
heads_opal_bin := $(build)/heads-opal/heads-opal
heads_opal_config_stamp := $(build)/heads-opal/.config-validated

$(heads_opal_config_stamp): $(linux_kconfig) $(CONFIG) Makefile
@mkdir -p "$(dir $@)"
@if [ "$(CONFIG_HEADS_OPAL_TEST_FIXTURE)" != "y" ] && \
! grep -q '^CONFIG_BLK_SED_OPAL=y$$' "$<"; then \
echo "CONFIG_HEADS_OPAL requires CONFIG_BLK_SED_OPAL=y in $<" >&2; \
exit 1; \
fi
@if [ "$(CONFIG_HEADS_OPAL_TEST_FIXTURE)" != "y" ] && \
[ "$(CONFIG_HEADS_OPAL_S3_APMC_V1)" = "y" ] && \
! grep -q '^CONFIG_PROC_PAGE_MONITOR=y$$' "$<"; then \
echo "CONFIG_HEADS_OPAL_S3_APMC_V1 requires CONFIG_PROC_PAGE_MONITOR=y in $<" >&2; \
exit 1; \
fi
@touch "$@"

$(heads_opal_bin): util/heads-opal.c $(heads_opal_config_stamp) \
$(INSTALL)/include/linux/limits.h \
$(build)/$(musl-cross-make_dir)/.build
@mkdir -p "$(dir $@)"
$(heads_cc) -Os -Wall -Wextra -Werror \

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can this be built with Oz?

$(if $(filter y,$(CONFIG_HEADS_OPAL_S3_APMC_V1)),-DHEADS_OPAL_S3_APMC_V1) \
-o "$@" "$<"

$(eval $(call initrd_bin_add,$(heads_opal_bin)))
endif

$(foreach m, $(bin_modules-y), \
$(call map,initrd_bin_add,$(call bins,$m)) \
)
Expand Down
66 changes: 66 additions & 0 deletions boards/qemu-coreboot-fbwhiptail-tpm2-opal/initrd/bin/heads-opal
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
#!/bin/bash

state_file=/tmp/heads-opal-qemu-unlocked
device=/dev/nvme0n1

log_serial() {
printf '%s\n' "$*" >/dev/ttyS0 2>/dev/null || true
}

case "${1:-}" in
scan)
log_serial "HEADS_OPAL_QEMU: mock scan completed"
if [ -e "$state_file" ]; then
printf '%s unlocked\n' "$device"
else
printf '%s locked\n' "$device"
fi
;;
status)
[ "${2:-}" = "$device" ] || exit 1
if [ -e "$state_file" ]; then
printf '%s\n' unlocked
log_serial "HEADS_OPAL_QEMU: post-unlock state confirmed"
else
printf '%s\n' locked
fi
;;
unlock)
[ "${2:-}" = "$device" ] || exit 1
[ "$#" -eq 2 ] || exit 1
if [ -e /tmp/secret/heads-opal-password ]; then
log_serial "HEADS_OPAL_QEMU: password file remained linked during unlock"
exit 1
fi
password=$(cat)
case "$(tr '\0' ' ' </proc/$$/cmdline)" in
*qemu-opal-password*)
log_serial "HEADS_OPAL_QEMU: password leaked through argv"
exit 1
;;
esac
if env | grep -q 'qemu-opal-password'; then
log_serial "HEADS_OPAL_QEMU: password leaked through environment"
exit 1
fi
if [ "$password" != "qemu-opal-password" ]; then
unset password
exit 3
fi
unset password
touch "$state_file"
log_serial "HEADS_OPAL_QEMU: unlock and S3 handoff accepted"
;;
lock)
[ "${2:-}" = "$device" ] || exit 1
[ "$#" -eq 2 ] || exit 1
password=$(cat)
[ "$password" = "qemu-opal-password" ] || exit 3
unset password
rm -f "$state_file"
log_serial "HEADS_OPAL_QEMU: rollback lock and S3 clear accepted"
;;
*)
exit 64
;;
esac
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
#!/bin/bash

printf '%s' 'qemu-opal-password'
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# QEMU fixture for the Heads TCG Opal boot gate. QEMU does not emulate an
# Opal drive, so board overlays provide a stateful mock backend and prompt.
include $(pwd)/boards/qemu-coreboot-fbwhiptail-tpm2/qemu-coreboot-fbwhiptail-tpm2.config

export CONFIG_BOARD_NAME="qemu-coreboot-fbwhiptail-tpm2-opal"
export CONFIG_HEADS_OPAL=y
export CONFIG_HEADS_OPAL_S3_APMC_V1=y
export CONFIG_HEADS_OPAL_TEST_FIXTURE=y
2 changes: 2 additions & 0 deletions doc/config.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,8 @@ These are not intended to be changed in user config.
| CONFIG_CRYPTSETUP2 | cryptsetup2 tools (used for LUKS) |
| CONFIG_DROPBEAR | DropBear SSH server (for debug / troubleshooting) |
| CONFIG_FBWHIPTAIL | fbwhiptail, framebuffer-based graphical whiptail implementation. Alternative is CONFIG_NEWT |
| CONFIG_HEADS_OPAL | Build an immutable gate for every boot mode that discovers and unlocks TCG Opal disks. Requires Linux 6.1 or later with CONFIG_BLK_SED_OPAL=y. |
| CONFIG_HEADS_OPAL_S3_APMC_V1 | Compile coreboot's OPAL S3 APMC ABI v1 into the helper. Requires compatible coreboot and CONFIG_PROC_PAGE_MONITOR=y. |

:point_right: TODO: document these:

Expand Down
1 change: 1 addition & 0 deletions doc/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ Quick reference: read the relevant doc when working on a topic.
| `gpg.md` | GPG tool operation for firmware signing and verification |
| `hotp.md` | HOTP-based remote attestation of firmware state |
| `keys.md` | Key management for firmware signing |
| `opal.md` | TCG Opal disk unlock and optional coreboot S3 credential handoff |
| `security-model.md` | TPM measured boot, trust chain, flash write protection |
| `TPM_GPIO_Reset_Approaches.md` | Eight approaches for resetting TPM via GPIO |
| `TPM_GPIO_Reset_Vulnerability.md` | TPM GPIO reset vulnerability analysis |
Expand Down
59 changes: 59 additions & 0 deletions doc/opal.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# TCG Opal disk unlock

Heads can unlock TCG Opal disks before any normal, basic, or network boot
script starts. Enable the boot gate with:

```make
export CONFIG_HEADS_OPAL=y
```

The board must use Linux 6.1 or later and its Linux configuration must enable
`CONFIG_BLK_SED_OPAL=y`. The build rejects a production configuration that
enables the gate without this kernel support. The `heads-opal` helper uses the
kernel's `IOC_OPAL_GET_STATUS` and `IOC_OPAL_LOCK_UNLOCK` interfaces. It
unlocks the global locking range as `Admin1`.

The gate is keyed to the helper installed in the immutable initrd, not the
mutable runtime user configuration. Production always uses `/bin/heads-opal`
and the built-in password dialog. A board may replace those fixed paths in its
initrd overlay for a fixture, but user configuration cannot select another
backend or disable the gate.

The prompt completes before the unlock begins. Heads opens and unlinks its
mode-0600 password file before passing the anonymous file descriptor to the C
helper, so the password is not placed in command arguments, the environment,
or a production Bash variable. The helper locks and clears its credential
buffers. If a later disk or S3 handoff fails, Heads uses the retained anonymous
descriptors to relock earlier disks before recovery. A rollback failure powers
the machine off instead of exposing an unlocked disk to a recovery shell.

coreboot provides the optional OPAL S3 APMC service through
[change 91045](https://review.coreboot.org/c/coreboot/+/91045). Boards built
with that service can explicitly select its version 1 ABI with:

```make
export CONFIG_HEADS_OPAL_S3_APMC_V1=y
```

Only enable this option when the running coreboot contains that ABI: APMC port
`0xb2`, command `0xee`, context signature `OPS3`, and context version 1. The
option makes failure to install or clear the password in coreboot SMM fatal.
The Linux configuration must additionally enable `CONFIG_PROC_PAGE_MONITOR=y`,
and the ABI limits passwords to 32 bytes. This selection is compiled into the
helper and cannot be disabled through runtime user configuration. The S3
handoff path currently supports NVMe devices only; ordinary cold-boot unlock
uses the kernel OPAL interface for any supported block device.

## QEMU fixture

`qemu-coreboot-fbwhiptail-tpm2-opal` overlays the fixed helper and prompt paths
with deterministic fixtures. Its explicit `CONFIG_HEADS_OPAL_TEST_FIXTURE=y`
setting is the only exception to the production kernel-configuration check. It
exercises Heads'
scan parsing, secret transport, unlock and rollback gates, post-unlock status
check, S3-handoff request, and transition to the boot script.

Upstream QEMU does not emulate a TCG Opal device or this SMM service. The
fixture therefore does not exercise the production OPAL ioctls, NVMe Security
Receive, physical-address lookup, or SMI handler. Those operations and S3
resume require hardware validation on a system with an Opal SSD.
Loading