diff --git a/.circleci/config.yml b/.circleci/config.yml index 191855454..f34b2665a 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -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) # ═══════════════════════════════════════════════════════════════════════════ @@ -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: @@ -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 diff --git a/Makefile b/Makefile index 49d1c9eac..f800a39e6 100644 --- a/Makefile +++ b/Makefile @@ -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 \ + $(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)) \ ) diff --git a/boards/qemu-coreboot-fbwhiptail-tpm2-opal/initrd/bin/heads-opal b/boards/qemu-coreboot-fbwhiptail-tpm2-opal/initrd/bin/heads-opal new file mode 100755 index 000000000..d72ef8078 --- /dev/null +++ b/boards/qemu-coreboot-fbwhiptail-tpm2-opal/initrd/bin/heads-opal @@ -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' ' ' "$output_file"; then + WARN "Unable to clear TCG Opal password file" + return 1 + fi + if ! rm -f "$output_file"; then + WARN "Unable to remove TCG Opal password file" + return 1 + fi +} + +opal_prompt_password() { + local device="$1" + local output_file="$2" + local rc + + umask 077 + opal_discard_password_file "$output_file" || return 1 + if [ -x "$opal_prompt" ]; then + "$opal_prompt" "$device" >"$output_file" + rc=$? + elif whiptail_type normal --title "TCG Opal Disk Unlock" \ + --passwordbox "Enter the TCG Opal password for $device" 0 80 \ + 2>"$output_file"; then + rc=0 + else + rc=$? + fi + if [ "$rc" -eq 0 ] && [ ! -s "$output_file" ]; then + rc=5 + fi + return "$rc" +} + +opal_close_password_fd() { + local password_fd="$1" + local rc=0 + + if ! : >"/proc/self/fd/$password_fd"; then + WARN "Unable to clear anonymous TCG Opal password storage" + rc=1 + fi + exec {password_fd}>&- + return "$rc" +} + +opal_relock_device() { + local tool="$1" + local device="$2" + local password_fd="$3" + local state + if ! "$tool" lock "$device" <"/proc/self/fd/$password_fd" \ + >>/tmp/heads-opal-output 2>&1; then + WARN "Unable to relock TCG Opal disk $device" + return 1 + fi + state=$("$tool" status "$device" 2>>/tmp/heads-opal-output) || return 1 + if [ "$state" != "locked" ]; then + WARN "$device remained in TCG Opal state '$state' after rollback" + return 1 + fi + return 0 +} + +heads_opal_unlock_device() { + local tool="$1" + local device="$2" + local attempts=3 + local attempt=1 + local output_file="$opal_password_file" + local password_fd + local prompt_rc + local rc + local state + + HEADS_OPAL_PASSWORD_FD= + + while [ "$attempt" -le "$attempts" ]; do + opal_prompt_password "$device" "$output_file" + prompt_rc=$? + + case "$prompt_rc" in + 0) + ;; + 5) + opal_discard_password_file "$output_file" || return 2 + whiptail_error --title "TCG Opal Unlock Failed" \ + --msgbox "The disk password cannot be empty." 0 80 + attempt=$((attempt + 1)) + continue + ;; + *) + opal_discard_password_file "$output_file" || true + WARN "TCG Opal unlock cancelled for $device" + return 1 + ;; + esac + + if ! exec {password_fd}<>"$output_file"; then + WARN "Unable to open TCG Opal password storage" + opal_discard_password_file "$output_file" || true + return 1 + fi + if ! rm -f "$output_file"; then + WARN "Unable to unlink TCG Opal password storage" + opal_close_password_fd "$password_fd" || true + return 1 + fi + "$tool" unlock "$device" <&"$password_fd" \ + >/tmp/heads-opal-output 2>&1 + rc=$? + + case "$rc" in + 0) + state=$("$tool" status "$device" 2>>/tmp/heads-opal-output) || { + WARN "Unable to confirm TCG Opal state for $device" + opal_relock_device "$tool" "$device" "$password_fd" || return 2 + opal_close_password_fd "$password_fd" || return 2 + return 1 + } + if [ "$state" != "unlocked" ]; then + WARN "$device remained in TCG Opal state '$state' after unlock" + opal_relock_device "$tool" "$device" "$password_fd" || return 2 + opal_close_password_fd "$password_fd" || return 2 + return 1 + fi + STATUS_OK "Unlocked TCG Opal disk $device" + HEADS_OPAL_PASSWORD_FD="$password_fd" + return 0 + ;; + 3) + opal_close_password_fd "$password_fd" || return 2 + whiptail_error --title "TCG Opal Unlock Failed" \ + --msgbox "The password for $device was rejected. Attempt $attempt of $attempts." 0 80 + ;; + 4) + opal_close_password_fd "$password_fd" || return 2 + WARN "TCG Opal S3 metadata or compatibility check failed for $device before unlock" + whiptail_error --title "TCG Opal Resume Protection Failed" \ + --msgbox "The disk was not unlocked because its firmware resume metadata could not be prepared." 0 80 + return 1 + ;; + 5) + if ! opal_relock_device "$tool" "$device" "$password_fd"; then + opal_close_password_fd "$password_fd" || true + return 2 + fi + opal_close_password_fd "$password_fd" || return 2 + WARN "TCG Opal S3 handoff failed for $device; the disk was relocked" + whiptail_error --title "TCG Opal Resume Protection Failed" \ + --msgbox "The firmware resume handoff failed, so the disk was relocked and boot has stopped." 0 80 + return 1 + ;; + *) + if ! opal_relock_device "$tool" "$device" "$password_fd"; then + opal_close_password_fd "$password_fd" || true + return 2 + fi + opal_close_password_fd "$password_fd" || return 2 + WARN "TCG Opal backend failed for $device (status $rc)" + return 1 + ;; + esac + attempt=$((attempt + 1)) + done + + WARN "TCG Opal password retries exhausted for $device" + return 1 +} + +heads_opal_main() { + local tool="$opal_tool" + local scan_output + local device + local state + local extra + local found_locked=n + local unlock_rc + local index + local -a unlocked_devices=() + local -a password_fds=() + + if [ ! -x "$tool" ]; then + WARN "TCG Opal backend is missing: $tool" + return 1 + fi + if ! scan_output=$("$tool" scan 2>/tmp/heads-opal-output); then + WARN "Unable to scan disks for TCG Opal state" + return 1 + fi + + while read -r device state extra; do + [ -n "$device" ] || continue + if [ -n "$extra" ]; then + WARN "Invalid TCG Opal scan result" + return 1 + fi + case "$state" in + disabled | unlocked | locked) + ;; + *) + WARN "Unknown TCG Opal state '$state' for $device" + return 1 + ;; + esac + done <<<"$scan_output" + + while read -r device state extra; do + [ -n "$device" ] || continue + case "$state" in + disabled | unlocked) + ;; + locked) + found_locked=y + heads_opal_unlock_device "$tool" "$device" + unlock_rc=$? + if [ "$unlock_rc" -ne 0 ]; then + for ((index = ${#unlocked_devices[@]} - 1; index >= 0; index--)); do + opal_relock_device "$tool" "${unlocked_devices[index]}" \ + "${password_fds[index]}" || unlock_rc=2 + opal_close_password_fd "${password_fds[index]}" || unlock_rc=2 + done + return "$unlock_rc" + fi + unlocked_devices+=("$device") + password_fds+=("$HEADS_OPAL_PASSWORD_FD") + ;; + esac + done <<<"$scan_output" + + for password_fd in "${password_fds[@]}"; do + opal_close_password_fd "$password_fd" || return 2 + done + + if [ "$found_locked" = "y" ]; then + STATUS_OK "All locked TCG Opal disks are ready" + fi + return 0 +} + +heads_opal_main "$@" diff --git a/initrd/init b/initrd/init index 74ac6cfcd..a53515167 100755 --- a/initrd/init +++ b/initrd/init @@ -256,6 +256,18 @@ else DEBUG "No board-init.sh found; skipping board-specific init" fi +if [ -x /bin/heads-opal ]; then + /bin/heads-opal-unlock.sh + opal_rc=$? + if [ "$opal_rc" -eq 2 ]; then + WARN "TCG Opal rollback failed; powering off without entering recovery" + poweroff -f + while true; do sleep 3600; done + elif [ "$opal_rc" -ne 0 ]; then + recovery "TCG Opal disk unlock failed" + fi +fi + if [ ! -x "$CONFIG_BOOTSCRIPT" -a ! -x "$CONFIG_BOOTSCRIPT_NETWORK" ]; then DEBUG "No boot script found (CONFIG_BOOTSCRIPT=$CONFIG_BOOTSCRIPT, CONFIG_BOOTSCRIPT_NETWORK=$CONFIG_BOOTSCRIPT_NETWORK); entering recovery" recovery 'Boot script missing? Entering recovery shell' diff --git a/tests/opal/test_heads_opal.c b/tests/opal/test_heads_opal.c new file mode 100644 index 000000000..d5f781c48 --- /dev/null +++ b/tests/opal/test_heads_opal.c @@ -0,0 +1,132 @@ +// SPDX-License-Identifier: GPL-2.0-only + +#include +#include +#include +#include + +#define main heads_opal_program_main +#include "../../util/heads-opal.c" +#undef main + +static void test_unlock_request(void) +{ + const uint8_t password[] = "correct horse"; + struct opal_lock_unlock request; + + fill_lock_request(&request, password, sizeof(password) - 1, OPAL_RW); + assert(request.session.who == OPAL_ADMIN1); + assert(request.session.sum == 0); + assert(request.session.opal_key.lr == 0); + assert(request.session.opal_key.key_len == sizeof(password) - 1); + assert(memcmp(request.session.opal_key.key, password, + sizeof(password) - 1) == 0); + assert(request.l_state == OPAL_RW); + + fill_lock_request(&request, password, sizeof(password) - 1, OPAL_LK); + assert(request.l_state == OPAL_LK); +} + +static void test_status_names(void) +{ + struct opal_status status = {0}; + + assert(strcmp(status_name(&status), "disabled") == 0); + status.flags = OPAL_FL_LOCKING_SUPPORTED | OPAL_FL_LOCKING_ENABLED | + OPAL_FL_LOCKED; + assert(strcmp(status_name(&status), "locked") == 0); + status.flags &= ~OPAL_FL_LOCKED; + assert(strcmp(status_name(&status), "unlocked") == 0); +} + +static void test_error_classes(void) +{ + assert(unsupported_errno(ENOTTY)); + assert(unsupported_errno(EOPNOTSUPP)); + assert(!unsupported_errno(EINVAL)); + assert(!unsupported_errno(EIO)); + assert(unavailable_errno(ENOMEDIUM)); + assert(unavailable_errno(ENODEV)); + assert(unavailable_errno(ENXIO)); + assert(!unavailable_errno(EACCES)); +} + +static void test_discovery_parser(void) +{ + uint8_t discovery[64] = {0}; + uint16_t comid = 0; + + discovery[3] = 56; + discovery[48] = 0x02; + discovery[49] = 0x03; + discovery[51] = 4; + discovery[52] = 0x12; + discovery[53] = 0x34; + assert(parse_discovery_comid(discovery, sizeof(discovery), &comid) == 0); + assert(comid == 0x1234); + + discovery[51] = 16; + assert(parse_discovery_comid(discovery, sizeof(discovery), &comid) != 0); + discovery[51] = 4; + discovery[3] = 47; + assert(parse_discovery_comid(discovery, sizeof(discovery), &comid) != 0); +} + +static void test_pci_parser(void) +{ + struct pci_bdf bdf = {0}; + + assert(parse_pci_component("0000:5d:1f.7", &bdf) == 0); + assert(bdf.bus == 0x5d); + assert(bdf.device == 0x1f); + assert(bdf.function == 7); + assert(parse_pci_component("0001:00:01.0", &bdf) != 0); + assert(parse_pci_component("0000:00:20.0", &bdf) != 0); + assert(parse_pci_component("not-pci", &bdf) != 0); +} + +static void test_password_reader(void) +{ + uint8_t password[4] = {0}; + size_t length = 0; + int input[2]; + int saved_stdin; + + assert(pipe(input) == 0); + assert(write(input[1], "abc\n", 4) == 4); + close(input[1]); + saved_stdin = dup(STDIN_FILENO); + assert(saved_stdin >= 0); + assert(dup2(input[0], STDIN_FILENO) == STDIN_FILENO); + close(input[0]); + assert(read_password(password, 3, &length) == 0); + assert(length == 3); + assert(memcmp(password, "abc", 3) == 0); + assert(dup2(saved_stdin, STDIN_FILENO) == STDIN_FILENO); + close(saved_stdin); + + memset(password, 0, sizeof(password)); + length = 0; + assert(pipe(input) == 0); + assert(write(input[1], "abcd", 4) == 4); + close(input[1]); + saved_stdin = dup(STDIN_FILENO); + assert(saved_stdin >= 0); + assert(dup2(input[0], STDIN_FILENO) == STDIN_FILENO); + close(input[0]); + assert(read_password(password, 3, &length) == HEADS_OPAL_ERROR); + assert(dup2(saved_stdin, STDIN_FILENO) == STDIN_FILENO); + close(saved_stdin); +} + +int main(void) +{ + test_unlock_request(); + test_status_names(); + test_error_classes(); + test_discovery_parser(); + test_pci_parser(); + test_password_reader(); + puts("heads-opal C tests: PASS"); + return 0; +} diff --git a/tests/opal/test_heads_opal.sh b/tests/opal/test_heads_opal.sh new file mode 100755 index 000000000..63e62a437 --- /dev/null +++ b/tests/opal/test_heads_opal.sh @@ -0,0 +1,234 @@ +#!/bin/bash + +set -euo pipefail + +repo=$(cd "$(dirname "$0")/../.." && pwd) +test_root="$repo/.opal-test.$$" +trap 'rm -rf "$test_root"' EXIT +mkdir -p "$test_root" + +cat >"$test_root/functions.sh" <<'EOF' +STATUS_OK() { printf 'STATUS_OK: %s\n' "$*"; } +WARN() { printf 'WARN: %s\n' "$*" >&2; } +EOF + +cat >"$test_root/gui_functions.sh" <<'EOF' +whiptail_error() { printf 'WHIPTAIL_ERROR\n' >&2; } +EOF + +cat >"$test_root/prompt" <<'EOF' +#!/bin/bash +count_file="$OPAL_TEST_ROOT/prompt-count" +count=$(cat "$count_file" 2>/dev/null || echo 0) +count=$((count + 1)) +printf '%s' "$count" >"$count_file" +touch "$OPAL_TEST_ROOT/prompt-called" +if [ "${PROMPT_CANCEL:-n}" = y ]; then + exit 1 +fi +if [ "${PROMPT_EMPTY:-n}" = y ]; then + exit 5 +fi +if [ "${PROMPT_FAIL_AFTER_OUTPUT:-n}" = y ]; then + printf '%s' correct-password + exit 1 +fi +if [ "${PROMPT_ALWAYS_WRONG:-n}" = y ] || + { [ "${PROMPT_WRONG_FIRST:-n}" = y ] && [ "$count" -eq 1 ]; }; then + printf '%s' wrong-password +else + printf '%s' correct-password +fi +EOF +chmod +x "$test_root/prompt" + +cat >"$test_root/backend" <<'EOF' +#!/bin/bash +set -u +printf '%s\n' "$*" >>"$OPAL_TEST_ROOT/backend-args" +case "${1:-}" in +scan) + case "$SCENARIO" in + none) ;; + unlocked) printf '/dev/mock0 unlocked\n' ;; + locked | wrong-first | always-wrong | backend-fail | s3-pre-fail | s3-fail | relock-fail) + if [ -e "$OPAL_TEST_ROOT/unlocked" ]; then + printf '/dev/mock0 unlocked\n' + else + printf '/dev/mock0 locked\n' + fi + ;; + multiple-later-fail) + for dev in mock0 mock1; do + if [ -e "$OPAL_TEST_ROOT/$dev-unlocked" ]; then + printf '/dev/%s unlocked\n' "$dev" + else + printf '/dev/%s locked\n' "$dev" + fi + done + ;; + multiple) + for dev in mock0 mock1; do + if [ -e "$OPAL_TEST_ROOT/$dev-unlocked" ]; then + printf '/dev/%s unlocked\n' "$dev" + else + printf '/dev/%s locked\n' "$dev" + fi + done + ;; + malformed) printf '/dev/mock0 locked extra\n' ;; + scan-fail) exit 1 ;; + esac + ;; +status) + name=${2##*/} + if [ -e "$OPAL_TEST_ROOT/unlocked" ] || + [ -e "$OPAL_TEST_ROOT/$name-unlocked" ]; then + printf 'unlocked\n' + else + printf 'locked\n' + fi + ;; +unlock) + device=${2:-} + [ "$#" -eq 2 ] || exit 1 + [ "$SCENARIO" != backend-fail ] || exit 1 + [ "$SCENARIO" != relock-fail ] || exit 1 + [ "$SCENARIO" != s3-pre-fail ] || exit 4 + [ ! -e "$OPAL_TEST_ROOT/password" ] || exit 1 + if [ "$SCENARIO" = multiple-later-fail ] && [ "$device" = /dev/mock1 ]; then + exit 1 + fi + password=$(cat) + case "$(tr '\0' ' ' >"$OPAL_TEST_ROOT/unlock-devices" + if [ "$password" != correct-password ]; then + unset password + exit 3 + fi + unset password + if [ "$SCENARIO" = multiple ] || [ "$SCENARIO" = multiple-later-fail ]; then + touch "$OPAL_TEST_ROOT/${device##*/}-unlocked" + else + touch "$OPAL_TEST_ROOT/unlocked" + fi + [ "$SCENARIO" != s3-fail ] || exit 5 + ;; +lock) + device=${2:-} + [ "$#" -eq 2 ] || exit 1 + [ ! -e "$OPAL_TEST_ROOT/password" ] || exit 1 + printf '%s\n' "$device" >>"$OPAL_TEST_ROOT/lock-devices" + password=$(cat) + [ "$password" = correct-password ] || exit 3 + unset password + [ "$SCENARIO" != relock-fail ] || exit 1 + rm -f "$OPAL_TEST_ROOT/unlocked" \ + "$OPAL_TEST_ROOT/${device##*/}-unlocked" + ;; +*) exit 64 ;; +esac +EOF +chmod +x "$test_root/backend" + +run_case() { + local scenario="$1" + rm -f "$test_root"/{backend-args,prompt-count,prompt-called,unlocked,unlock-devices,lock-devices,mock0-unlocked,mock1-unlocked} + OPAL_TEST_ROOT="$test_root" \ + SCENARIO="$scenario" \ + PROMPT_WRONG_FIRST="${PROMPT_WRONG_FIRST:-n}" \ + PROMPT_ALWAYS_WRONG="${PROMPT_ALWAYS_WRONG:-n}" \ + PROMPT_CANCEL="${PROMPT_CANCEL:-n}" \ + PROMPT_EMPTY="${PROMPT_EMPTY:-n}" \ + PROMPT_FAIL_AFTER_OUTPUT="${PROMPT_FAIL_AFTER_OUTPUT:-n}" \ + "$repo/initrd/bin/heads-opal-unlock.sh" --test-fixture "$test_root" +} + +run_case none +[ ! -e "$test_root/prompt-called" ] + +run_case unlocked +[ ! -e "$test_root/prompt-called" ] + +run_case locked +[ "$(cat "$test_root/prompt-count")" = 1 ] +grep -qx 'unlock /dev/mock0' "$test_root/backend-args" + +PROMPT_WRONG_FIRST=y run_case wrong-first +[ "$(cat "$test_root/prompt-count")" = 2 ] + +if PROMPT_ALWAYS_WRONG=y run_case always-wrong; then + exit 1 +fi +[ "$(cat "$test_root/prompt-count")" = 3 ] + +if PROMPT_CANCEL=y run_case locked; then + exit 1 +fi +[ "$(cat "$test_root/prompt-count")" = 1 ] + +if PROMPT_EMPTY=y run_case locked; then + exit 1 +fi +[ "$(cat "$test_root/prompt-count")" = 3 ] +[ ! -e "$test_root/password" ] + +if PROMPT_FAIL_AFTER_OUTPUT=y run_case locked; then + exit 1 +fi +[ ! -e "$test_root/unlock-devices" ] +[ ! -e "$test_root/password" ] + +if run_case backend-fail; then + exit 1 +fi +[ "$(cat "$test_root/prompt-count")" = 1 ] + +set +e +run_case relock-fail +[ "$?" -eq 2 ] || exit 1 +set -e + +if run_case s3-pre-fail; then + exit 1 +fi +[ ! -e "$test_root/unlocked" ] +if run_case s3-fail; then + exit 1 +fi +grep -qx '/dev/mock0' "$test_root/lock-devices" +[ ! -e "$test_root/unlocked" ] +if run_case scan-fail; then + exit 1 +fi +if run_case malformed; then + exit 1 +fi + +run_case multiple +[ "$(wc -l <"$test_root/unlock-devices")" -eq 2 ] + +if run_case multiple-later-fail; then + exit 1 +fi +grep -qx '/dev/mock0' "$test_root/lock-devices" +[ ! -e "$test_root/mock0-unlocked" ] + +opal_line=$(grep -n '/bin/heads-opal-unlock.sh' "$repo/initrd/init" | cut -d: -f1) +bootscript_line=$(grep -n '^if \[ ! -x ' "$repo/initrd/init" | cut -d: -f1) +[ "$opal_line" -lt "$bootscript_line" ] +grep -q '^if \[ -x /bin/heads-opal \]' "$repo/initrd/init" +grep -q 'heads-opal-unlock.sh' "$repo/initrd/bin/gui-init.sh" && exit 1 + +tmpdir="$test_root/compiler-tmp" +mkdir -p "$tmpdir" +TMPDIR="$tmpdir" gcc -std=gnu11 -O2 -Wall -Wextra -Werror \ + -o "$test_root/test-heads-opal" "$repo/tests/opal/test_heads_opal.c" +"$test_root/test-heads-opal" + +printf 'heads-opal shell tests: PASS\n' diff --git a/util/heads-opal.c b/util/heads-opal.c new file mode 100644 index 000000000..749c0b84a --- /dev/null +++ b/util/heads-opal.c @@ -0,0 +1,638 @@ +// SPDX-License-Identifier: GPL-2.0-only + +#define _GNU_SOURCE + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#if defined(__x86_64__) +#include +#endif + +#define ARRAY_SIZE(array) (sizeof(array) / sizeof((array)[0])) + +#define OPAL_DISCOVERY_BUFFER_SIZE 2048 +#define OPAL_DISCOVERY_HEADER_SIZE 48 +#define OPAL_DISCOVERY_COMID 0x0001 +#define OPAL_SECURITY_PROTOCOL 0x01 +#define OPAL_FEATURE_V100 0x0200 +#define OPAL_FEATURE_V200 0x0203 +#define OPAL_METHOD_NOT_AUTHORIZED 0x01 + +#define NVME_ADMIN_SECURITY_RECEIVE 0x82 + +#define OPAL_S3_APM_PORT 0xb2 +#define OPAL_S3_APM_COMMAND 0xee +#define OPAL_S3_SUBCOMMAND_SET_SECRET 0x01 +#define OPAL_S3_SUBCOMMAND_CLEAR_SECRET 0x02 +#define OPAL_S3_CONTEXT_SIGNATURE 0x3353504fU +#define OPAL_S3_CONTEXT_VERSION 0x0001 +#define OPAL_S3_PASSWORD_MAX 32 +#define OPAL_S3_PAGE_ATTEMPTS 256 + +#if defined(HEADS_OPAL_S3_APMC_V1) +#define OPAL_S3_ENABLED true +#else +#define OPAL_S3_ENABLED false +#endif + +enum { + HEADS_OPAL_ERROR = 1, + HEADS_OPAL_UNSUPPORTED = 2, + HEADS_OPAL_AUTH_FAILED = 3, + HEADS_OPAL_S3_PRE_UNLOCK_FAILED = 4, + HEADS_OPAL_S3_POST_UNLOCK_FAILED = 5, + HEADS_OPAL_USAGE = 64, +}; + +struct opal_s3_context { + uint32_t signature; + uint16_t version; + uint16_t size; + uint8_t bus; + uint8_t device; + uint8_t function; + uint8_t reserved0; + uint16_t base_comid; + uint16_t reserved1; + uint8_t password_length; + uint8_t reserved2[3]; + uint8_t password[OPAL_S3_PASSWORD_MAX]; +} __attribute__((packed)); + +_Static_assert(sizeof(struct opal_s3_context) == 52, + "coreboot OPAL S3 context ABI changed"); + +struct pci_bdf { + uint8_t bus; + uint8_t device; + uint8_t function; +}; + +static void secure_clear(void *buffer, size_t length) +{ + volatile uint8_t *cursor = buffer; + + while (length-- > 0) + *cursor++ = 0; +} + +static uint16_t read_be16(const uint8_t *buffer) +{ + return ((uint16_t)buffer[0] << 8) | buffer[1]; +} + +static uint32_t read_be32(const uint8_t *buffer) +{ + return ((uint32_t)buffer[0] << 24) | + ((uint32_t)buffer[1] << 16) | + ((uint32_t)buffer[2] << 8) | + buffer[3]; +} + +static bool unsupported_errno(int error) +{ + return error == ENOTTY || error == EOPNOTSUPP; +} + +static bool unavailable_errno(int error) +{ + return error == ENOMEDIUM || error == ENODEV || error == ENXIO; +} + +static int get_opal_status(int fd, struct opal_status *status) +{ + memset(status, 0, sizeof(*status)); + if (ioctl(fd, IOC_OPAL_GET_STATUS, status) == 0) + return 0; + if (unsupported_errno(errno)) + return HEADS_OPAL_UNSUPPORTED; + return HEADS_OPAL_ERROR; +} + +static const char *status_name(const struct opal_status *status) +{ + if (!(status->flags & OPAL_FL_LOCKING_SUPPORTED) || + !(status->flags & OPAL_FL_LOCKING_ENABLED)) + return "disabled"; + if (status->flags & OPAL_FL_LOCKED) + return "locked"; + return "unlocked"; +} + +static int status_device(const char *path, bool print_path) +{ + struct opal_status status; + int fd; + int rc; + + fd = open(path, O_RDONLY | O_CLOEXEC); + if (fd < 0) { + if (print_path && unavailable_errno(errno)) + return HEADS_OPAL_UNSUPPORTED; + fprintf(stderr, "%s: open failed: %s\n", path, strerror(errno)); + return HEADS_OPAL_ERROR; + } + + rc = get_opal_status(fd, &status); + close(fd); + if (rc != 0) { + if (rc != HEADS_OPAL_UNSUPPORTED) + fprintf(stderr, "%s: OPAL status failed: %s\n", path, + strerror(errno)); + return rc; + } + + if (print_path) + printf("%s %s\n", path, status_name(&status)); + else + printf("%s\n", status_name(&status)); + return 0; +} + +static int scan_devices(void) +{ + glob_t blocks = {0}; + size_t index; + int glob_rc; + int rc = 0; + + glob_rc = glob("/sys/class/block/*", 0, NULL, &blocks); + if (glob_rc == GLOB_NOMATCH) + return 0; + if (glob_rc != 0) + return HEADS_OPAL_ERROR; + + for (index = 0; index < blocks.gl_pathc; index++) { + const char *name = strrchr(blocks.gl_pathv[index], '/'); + char partition_path[512]; + char device_path[512]; + char resolved[4096]; + int one_rc; + + if (!name || name[1] == '\0') + continue; + if (realpath(blocks.gl_pathv[index], resolved) && + strstr(resolved, "/virtual/block/")) + continue; + name++; + if (snprintf(partition_path, sizeof(partition_path), "%s/partition", + blocks.gl_pathv[index]) >= (int)sizeof(partition_path)) { + rc = HEADS_OPAL_ERROR; + break; + } + if (access(partition_path, F_OK) == 0) + continue; + if (snprintf(device_path, sizeof(device_path), "/dev/%s", name) >= + (int)sizeof(device_path)) { + rc = HEADS_OPAL_ERROR; + break; + } + + one_rc = status_device(device_path, true); + if (one_rc == HEADS_OPAL_UNSUPPORTED) + continue; + if (one_rc != 0) { + rc = one_rc; + break; + } + } + + globfree(&blocks); + return rc; +} + +static int read_password(uint8_t *password, size_t capacity, size_t *length) +{ + size_t used = 0; + bool overflow = false; + uint8_t byte; + ssize_t got; + + while ((got = read(STDIN_FILENO, &byte, 1)) == 1) { + if (byte == '\n') + break; + if (used >= capacity) { + overflow = true; + break; + } + password[used++] = byte; + } + if (got < 0) { + fprintf(stderr, "reading password failed: %s\n", strerror(errno)); + return HEADS_OPAL_ERROR; + } + if (overflow) { + fprintf(stderr, "password exceeds %zu bytes\n", capacity); + return HEADS_OPAL_ERROR; + } + if (used == 0) { + fprintf(stderr, "empty passwords are not accepted\n"); + return HEADS_OPAL_ERROR; + } + + *length = used; + return 0; +} + +static void fill_lock_request(struct opal_lock_unlock *request, + const uint8_t *password, size_t password_length, + enum opal_lock_state lock_state) +{ + memset(request, 0, sizeof(*request)); + request->session.who = OPAL_ADMIN1; + request->session.opal_key.lr = 0; + request->session.opal_key.key_len = (uint8_t)password_length; + memcpy(request->session.opal_key.key, password, password_length); + request->l_state = lock_state; +} + +static int parse_pci_component(const char *component, struct pci_bdf *bdf) +{ + unsigned int domain; + unsigned int bus; + unsigned int device; + unsigned int function; + int consumed = 0; + + if (sscanf(component, "%x:%x:%x.%x%n", &domain, &bus, &device, + &function, &consumed) != 4 || component[consumed] != '\0') + return -1; + if (domain != 0 || bus > UINT8_MAX || device > 0x1f || function > 7) + return -1; + + bdf->bus = (uint8_t)bus; + bdf->device = (uint8_t)device; + bdf->function = (uint8_t)function; + return 0; +} + +static int device_pci_bdf(int fd, struct pci_bdf *bdf) +{ + struct stat stat_buffer; + char link_path[128]; + char resolved[4096]; + char path_copy[4096]; + char *component; + char *saveptr = NULL; + int found = -1; + + if (fstat(fd, &stat_buffer) != 0 || !S_ISBLK(stat_buffer.st_mode)) + return -1; + if (snprintf(link_path, sizeof(link_path), "/sys/dev/block/%u:%u/device", + major(stat_buffer.st_rdev), minor(stat_buffer.st_rdev)) >= + (int)sizeof(link_path)) + return -1; + if (!realpath(link_path, resolved)) + return -1; + + memcpy(path_copy, resolved, strlen(resolved) + 1); + for (component = strtok_r(path_copy, "/", &saveptr); component; + component = strtok_r(NULL, "/", &saveptr)) { + struct pci_bdf candidate; + + if (parse_pci_component(component, &candidate) == 0) { + *bdf = candidate; + found = 0; + } + } + return found; +} + +static int parse_discovery_comid(const uint8_t *buffer, size_t buffer_length, + uint16_t *base_comid) +{ + size_t cursor = OPAL_DISCOVERY_HEADER_SIZE; + uint32_t discovery_length; + bool found = false; + + if (buffer_length < OPAL_DISCOVERY_HEADER_SIZE) + return -1; + discovery_length = read_be32(buffer); + if (discovery_length < OPAL_DISCOVERY_HEADER_SIZE || + discovery_length > buffer_length) + return -1; + + while (cursor < discovery_length) { + uint16_t code; + uint8_t length; + + if (discovery_length - cursor < 4) + return -1; + code = read_be16(buffer + cursor); + length = buffer[cursor + 3]; + cursor += 4; + if (length > discovery_length - cursor) + return -1; + + if ((code == OPAL_FEATURE_V100 || code == OPAL_FEATURE_V200) && + length >= 4) { + *base_comid = read_be16(buffer + cursor); + found = true; + } + cursor += length; + } + + return found ? 0 : -1; +} + +static int nvme_discovery_comid(int fd, uint16_t *base_comid) +{ + uint8_t buffer[OPAL_DISCOVERY_BUFFER_SIZE] = {0}; + struct nvme_admin_cmd command = {0}; + + command.opcode = NVME_ADMIN_SECURITY_RECEIVE; + command.nsid = 0; + command.addr = (uintptr_t)buffer; + command.data_len = sizeof(buffer); + command.cdw10 = ((uint32_t)OPAL_SECURITY_PROTOCOL << 24) | + ((uint32_t)OPAL_DISCOVERY_COMID << 8); + command.cdw11 = sizeof(buffer); + + if (ioctl(fd, NVME_IOCTL_ADMIN_CMD, &command) != 0) + return -1; + return parse_discovery_comid(buffer, sizeof(buffer), base_comid); +} + +static int virtual_to_physical(const void *address, uint64_t *physical) +{ + uint64_t entry; + long page_size = sysconf(_SC_PAGESIZE); + uintptr_t virtual_address = (uintptr_t)address; + off_t offset; + int fd; + + if (page_size <= 0) + return -1; + offset = (off_t)((virtual_address / (uintptr_t)page_size) * sizeof(entry)); + fd = open("/proc/self/pagemap", O_RDONLY | O_CLOEXEC); + if (fd < 0) + return -1; + if (pread(fd, &entry, sizeof(entry), offset) != (ssize_t)sizeof(entry)) { + close(fd); + return -1; + } + close(fd); + + if (!(entry & (UINT64_C(1) << 63))) + return -1; + entry &= (UINT64_C(1) << 55) - 1; + if (entry == 0) + return -1; + *physical = entry * (uint64_t)page_size + + virtual_address % (uintptr_t)page_size; + return 0; +} + +static void *allocate_smi_page(uint64_t *physical, size_t *page_size_out) +{ + void *pages[OPAL_S3_PAGE_ATTEMPTS] = {0}; + long page_size = sysconf(_SC_PAGESIZE); + void *selected = NULL; + size_t index; + + if (page_size <= 0) + return NULL; + for (index = 0; index < ARRAY_SIZE(pages); index++) { + pages[index] = mmap(NULL, (size_t)page_size, PROT_READ | PROT_WRITE, + MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + if (pages[index] == MAP_FAILED) { + pages[index] = NULL; + break; + } + memset(pages[index], 0, (size_t)page_size); + if (virtual_to_physical(pages[index], physical) == 0 && + *physical <= UINT32_MAX && + mlock(pages[index], (size_t)page_size) == 0 && + virtual_to_physical(pages[index], physical) == 0 && + *physical <= UINT32_MAX) { + selected = pages[index]; + break; + } + } + + for (index = 0; index < ARRAY_SIZE(pages); index++) { + if (!pages[index] || pages[index] == selected) + continue; + munmap(pages[index], (size_t)page_size); + } + if (selected) + *page_size_out = (size_t)page_size; + return selected; +} + +#if defined(__x86_64__) +static unsigned long trigger_smi(unsigned long command, unsigned long argument) +{ + unsigned long result = command; + + __asm__ volatile("outb %%al, $0xb2" + : "+a"(result) + : "b"(argument) + : "memory"); + return result; +} +#endif + +static int call_s3_service(uint8_t subcommand, unsigned long argument) +{ +#if defined(__x86_64__) + unsigned long command; + unsigned long result; + + if (ioperm(OPAL_S3_APM_PORT, 1, 1) != 0) { + fprintf(stderr, "enabling APMC access failed: %s\n", strerror(errno)); + return -1; + } + + command = ((unsigned long)subcommand << 8) | OPAL_S3_APM_COMMAND; + result = trigger_smi(command, argument); + ioperm(OPAL_S3_APM_PORT, 1, 0); + if (result == command) { + fprintf(stderr, "coreboot did not handle the OPAL S3 request\n"); + return -1; + } + if (result != 0) { + fprintf(stderr, "coreboot rejected the OPAL S3 request: 0x%lx\n", + result); + return -1; + } + return 0; +#else + (void)subcommand; + (void)argument; + fprintf(stderr, "OPAL S3 handoff is only supported on x86_64\n"); + return -1; +#endif +} + +static int clear_s3_secrets(void) +{ + return call_s3_service(OPAL_S3_SUBCOMMAND_CLEAR_SECRET, 0); +} + +static int handoff_s3_secret(const struct pci_bdf *bdf, uint16_t base_comid, + const uint8_t *password, size_t password_length) +{ +#if defined(__x86_64__) + struct opal_s3_context *context; + uint64_t physical = 0; + size_t page_size = 0; + int rc; + + context = allocate_smi_page(&physical, &page_size); + if (!context) { + fprintf(stderr, "could not allocate a low physical page for OPAL S3 handoff\n"); + return -1; + } + + context->signature = OPAL_S3_CONTEXT_SIGNATURE; + context->version = OPAL_S3_CONTEXT_VERSION; + context->size = sizeof(*context); + context->bus = bdf->bus; + context->device = bdf->device; + context->function = bdf->function; + context->base_comid = base_comid; + context->password_length = (uint8_t)password_length; + memcpy(context->password, password, password_length); + + rc = call_s3_service(OPAL_S3_SUBCOMMAND_SET_SECRET, + (unsigned long)physical); + + secure_clear(context, sizeof(*context)); + munlock(context, page_size); + munmap(context, page_size); + return rc; +#else + (void)bdf; + (void)base_comid; + (void)password; + (void)password_length; + fprintf(stderr, "OPAL S3 handoff is only supported on x86_64\n"); + return -1; +#endif +} + +static int change_lock_state(const char *path, bool unlock, bool s3_handoff) +{ + struct opal_lock_unlock request; + struct pci_bdf bdf = {0}; + uint8_t password[OPAL_KEY_MAX] = {0}; + uint16_t base_comid = 0; + size_t password_length = 0; + int fd = -1; + int ioctl_rc; + int rc; + bool s3_clear_failed = false; + + if (mlock(password, sizeof(password)) != 0) { + fprintf(stderr, "locking password memory failed: %s\n", strerror(errno)); + return HEADS_OPAL_ERROR; + } + fd = open(path, O_RDWR | O_CLOEXEC); + if (fd < 0) { + fprintf(stderr, "%s: open failed: %s\n", path, strerror(errno)); + rc = HEADS_OPAL_ERROR; + goto out; + } + + if (unlock && s3_handoff) { + if (device_pci_bdf(fd, &bdf) != 0 || + nvme_discovery_comid(fd, &base_comid) != 0) { + fprintf(stderr, "%s: could not obtain NVMe OPAL S3 metadata\n", path); + rc = HEADS_OPAL_S3_PRE_UNLOCK_FAILED; + goto out; + } + } + + rc = read_password(password, OPAL_KEY_MAX - 1, &password_length); + if (rc != 0) + goto out; + if (unlock && s3_handoff && password_length > OPAL_S3_PASSWORD_MAX) { + fprintf(stderr, "coreboot OPAL S3 handoff accepts at most %u bytes\n", + OPAL_S3_PASSWORD_MAX); + rc = HEADS_OPAL_S3_PRE_UNLOCK_FAILED; + goto out; + } + + if (!unlock && s3_handoff && clear_s3_secrets() != 0) + s3_clear_failed = true; + + fill_lock_request(&request, password, password_length, + unlock ? OPAL_RW : OPAL_LK); + ioctl_rc = ioctl(fd, IOC_OPAL_LOCK_UNLOCK, &request); + if (ioctl_rc != 0) { + if (ioctl_rc == OPAL_METHOD_NOT_AUTHORIZED) { + fprintf(stderr, "%s: OPAL password was not authorized\n", path); + rc = HEADS_OPAL_AUTH_FAILED; + } else if (ioctl_rc < 0) { + fprintf(stderr, "%s: OPAL %s failed: %s\n", path, + unlock ? "unlock" : "lock", strerror(errno)); + rc = HEADS_OPAL_ERROR; + } else { + fprintf(stderr, "%s: OPAL %s failed with method status 0x%x\n", + path, unlock ? "unlock" : "lock", ioctl_rc); + rc = HEADS_OPAL_ERROR; + } + goto out_request; + } + if (unlock && s3_handoff && + handoff_s3_secret(&bdf, base_comid, password, password_length) != 0) { + fill_lock_request(&request, password, password_length, OPAL_LK); + if (ioctl(fd, IOC_OPAL_LOCK_UNLOCK, &request) != 0) + fprintf(stderr, "%s: rollback lock failed after S3 handoff error\n", + path); + if (clear_s3_secrets() != 0) + fprintf(stderr, "clearing coreboot OPAL S3 secrets failed\n"); + rc = HEADS_OPAL_S3_POST_UNLOCK_FAILED; + goto out_request; + } + rc = s3_clear_failed ? HEADS_OPAL_ERROR : 0; + +out_request: + secure_clear(&request, sizeof(request)); +out: + secure_clear(password, sizeof(password)); + munlock(password, sizeof(password)); + if (fd >= 0) + close(fd); + return rc; +} + +static void usage(const char *program) +{ + fprintf(stderr, + "usage: %s scan\n" + " %s status DEVICE\n" + " %s unlock DEVICE\n" + " %s lock DEVICE\n", + program, program, program, program); +} + +int main(int argc, char **argv) +{ + if (argc == 2 && strcmp(argv[1], "scan") == 0) + return scan_devices(); + if (argc == 3 && strcmp(argv[1], "status") == 0) + return status_device(argv[2], false); + if (argc == 3 && strcmp(argv[1], "unlock") == 0) + return change_lock_state(argv[2], true, OPAL_S3_ENABLED); + if (argc == 3 && strcmp(argv[1], "lock") == 0) + return change_lock_state(argv[2], false, OPAL_S3_ENABLED); + + usage(argv[0]); + return HEADS_OPAL_USAGE; +}