From 6a767408b383bad223e8d99b0a64f7de9331a956 Mon Sep 17 00:00:00 2001 From: Finn Callies Date: Tue, 24 Mar 2026 10:37:22 +0100 Subject: [PATCH] ebc: Add new tool pvics MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit pvics (PV Image Customization Support) is a comprehensive tool for converting existing qcow2 KVM guest images to IBM Secure Execution for Linux (SEL) images with Early Boot Customization (EBC) support and encrypted root filesystems. The tool provides four main actions: - list: Retrieve information about boot loader entries in a base image - convert: Convert a base image to an EBC-ready SEL image - encrypt: Encrypt the root filesystem and prepare EBC resources - full: Perform both conversion and encryption in one operation Assisted-by: IBM Bob:1.0.1 Reviewed-by: Holger Dengler Reviewed-by: Jan Höppner Signed-off-by: Finn Callies Signed-off-by: Jan Höppner --- README.md | 4 + scripts/Makefile | 11 +- scripts/pvics | 1579 ++++++++++++++++++++++++++++++++++++++++++ scripts/pvics.8 | 340 +++++++++ scripts/pvics.yaml | 106 +++ scripts/pvics.yaml.5 | 321 +++++++++ 6 files changed, 2359 insertions(+), 2 deletions(-) create mode 100755 scripts/pvics create mode 100644 scripts/pvics.8 create mode 100644 scripts/pvics.yaml create mode 100644 scripts/pvics.yaml.5 diff --git a/README.md b/README.md index 5e75558e..91d61c5a 100644 --- a/README.md +++ b/README.md @@ -52,6 +52,10 @@ Package contents Create an IBM Secure Execution (protected virtualization) image. The genprotimg command is a symbolic link to the `pvimg create` command. + * pvics: + Convert a qcow2 image to a qcow2 image ready to boot as IBM Secure Execution + for Linux guest. + * udev rules: - 59-dasd.rules: rules for unique DASD device nodes created in /dev/disk/. - 57-osasnmpd.rules: udev rules for osasnmpd. diff --git a/scripts/Makefile b/scripts/Makefile index c08afc24..7c6e3971 100644 --- a/scripts/Makefile +++ b/scripts/Makefile @@ -1,9 +1,10 @@ include ../common.mak -SCRIPTS = dbginfo.sh zfcpdbf zipl-switch-to-blscfg sclpdbf +SCRIPTS = dbginfo.sh zfcpdbf zipl-switch-to-blscfg sclpdbf pvics # Helper scripts controlled by corresponding systemd services SD_HELPER_SCRIPTS = cpictl dumpconf -MAN_PAGES = dbginfo.sh.8 zfcpdbf.8 zipl-switch-to-blscfg.8 +MAN_PAGES = dbginfo.sh.8 zfcpdbf.8 zipl-switch-to-blscfg.8 pvics.8 +FILE_MAN_PAGES = pvics.yaml.5 all: @@ -33,6 +34,12 @@ install: $(DESTDIR)$(MANDIR)/man8; \ done + @for i in $(FILE_MAN_PAGES); \ + do \ + $(INSTALL) -g $(GROUP) -o $(OWNER) -m 644 $$i \ + $(DESTDIR)$(MANDIR)/man5; \ + done + clean: .zfcpdbf.ct: zfcpdbf diff --git a/scripts/pvics b/scripts/pvics new file mode 100755 index 00000000..a42a60bd --- /dev/null +++ b/scripts/pvics @@ -0,0 +1,1579 @@ +#!/bin/bash +# SPDX-License-Identifier: MIT +# +# Copyright IBM Corp. + + +# +# CLEANUP STACK FUNCTIONS +# + +# Initialize the cleanup stack as an array +declare -a CLEANUP_STACK=() + +cleanup_push() { + local cmd="$*" + if [[ -z "$cmd" ]]; then + log_error "pvics: no command provided" + return 1 + fi + CLEANUP_STACK+=("$cmd") + log_debug "registered cleanup: ${cmd}" + return 0 +} + +cleanup_pop() { + local stack_size=${#CLEANUP_STACK[@]} + + if [[ $stack_size -eq 0 ]]; then + log_warn "cleanup_pop: Warning - cleanup stack is empty" + return 1 + fi + + # Get the last command (top of stack) + local last_idx=$((stack_size - 1)) + local cmd="${CLEANUP_STACK[$last_idx]}" + + # Remove it from the stack + unset 'CLEANUP_STACK[$last_idx]' + + # Execute the command + log_debug "cleanup_pop: Executing: $cmd" + execute "${cmd}" + local ret=$? + + if [[ $ret -ne 0 ]]; then + log_warn "cleanup_pop: Warning - command failed with exit code $ret" + fi + + return $ret +} + +cleanup_run() { + local stack_size=${#CLEANUP_STACK[@]} + local failed=0 + + if [[ "${stack_size}" -eq 0 ]]; then + log_debug "cleanup_run: No cleanup commands to execute" + return 0 + fi + + log_debug "cleanup_run: Executing ${stack_size} cleanup command(s)" + + # Execute commands in reverse order (LIFO) + while [[ ${#CLEANUP_STACK[@]} -gt 0 ]]; do + if ! cleanup_pop; then + ((failed++)) + fi + done + + if [[ "${failed}" -gt 0 ]]; then + log_warn "cleanup_run: Warning - ${failed} command(s) failed" + return 1 + fi + + return 0 +} + +cleanup_clear() { + local stack_size=${#CLEANUP_STACK[@]} + CLEANUP_STACK=() + log_debug "cleanup_clear: Cleared $stack_size command(s) from stack" + return 0 +} + +cleanup_show() { + local stack_size=${#CLEANUP_STACK[@]} + + if [[ $stack_size -eq 0 ]]; then + log_info "Cleanup stack is empty" + return 0 + fi + + log_info "Cleanup stack ($stack_size command(s)):" + local idx=0 + for cmd in "${CLEANUP_STACK[@]}"; do + log_info " [$idx] $cmd" + ((idx++)) + done + + return 0 +} + +cleanup() { + local rc=${1:-1} + local stack_size=${#CLEANUP_STACK[@]} + + trap - ERR EXIT PIPE + + if [[ "${stack_size}" -gt 0 ]]; then + cleanup_run + log_info "Cleanup finished" + fi + + if [[ "${rc}" -ne 0 ]]; then + exit "${rc}" + fi + + return 0 +} + + +# +# LOGGING FUNCTIONS +# + +# Configuration for logger utility +LOGGER_TAG="pvics" +LOGGER_FACILITY="user" + +log_init() { + logger -t "${LOGGER_TAG}" -p "${LOGGER_FACILITY}.info" "Logging initialized" + return 0 +} + +log_debug() { + local msg="$*" + logger -t "${LOGGER_TAG}" -p "${LOGGER_FACILITY}.debug" "${msg}" + return 0 +} + +log_info() { + local msg="$*" + echo "[ INFO ] ${msg}" + logger -t "${LOGGER_TAG}" -p "${LOGGER_FACILITY}.info" "${msg}" + return 0 +} + +log_warn() { + local msg="$*" + echo "[ WARN ] ${msg}" + logger -t "${LOGGER_TAG}" -p "${LOGGER_FACILITY}.warning" "${msg}" + return 0 +} + +log_error() { + local msg="$*" + echo "[ ERR ] ${msg}" >&2 + logger -t "${LOGGER_TAG}" -p "${LOGGER_FACILITY}.err" "${msg}" + return 1 +} + +log_cleanup() { + logger -t "${LOGGER_TAG}" -p "${LOGGER_FACILITY}.info" "Logging cleanup complete" + return 0 +} + +setup_logger() { + log_init + cleanup_push log_cleanup + return 0 +} + +warn_overwrite() { + local file + + if [[ $# -lt 1 ]]; then + log_error "internal error" + fi + + file=$1 + + if [[ -e "${file}" ]]; then + log_warn "${file} already exists and may be overwritten" + fi + + return 0 +} + +err_noexist() { + local file + + if [[ $# -lt 1 ]]; then + return 0 + fi + + file=$1 + + if [[ ! -e "${file}" ]]; then + log_error "${file} does not exist" + fi + + return 0 +} + + +# +# PREREQ FUNCTIONS +# + +check_pvsecret_ebc() { + local pvsecret_version + + # Run pvsecret with --version --verbose to check for EBC support + pvsecret_version=$(pvsecret --version --verbose 2>&1 || true) + + if ! echo "${pvsecret_version}" | grep -q "+ebc"; then + log_error "pvsecret does not support EBC feature. Please use a version compiled with EBC support." + fi + + log_info "pvsecret supports EBC feature" + + return 0 +} + +check_prereqs_convert() { + local tools + + tools=( + "blkid" + "lsblk" + "lsinitrd" + "modprobe" + "mount" + "mountpoint" + "pvextract-hdr" + "pvimg" + "printf" + "qemu-nbd" + "systemctl" + "umount" + ) + + for tool in "${tools[@]}"; do + require_command "${tool}" + done + + # Check for virtualization tools based on architecture + if [[ "${ARCH}" == "s390x" ]]; then + require_command "virt-install" + require_command "virsh" + else + require_command "qemu-system-s390x" + require_command "pgrep" + fi + + return 0 +} + +check_prereqs_encrypt() { + local tools + + tools=( + "blkid" + "cryptsetup" + "hexdump" + "lsblk" + "mktemp" + "modprobe" + "mount" + "mountpoint" + "pvebc" + "pvextract-hdr" + "pvsecret" + "qemu-img" + "qemu-nbd" + "sha256sum" + "strings" + "umount" + "xxd" + "virt-resize" + ) + + for tool in "${tools[@]}"; do + require_command "${tool}" + done + + # Check if pvsecret supports EBC feature + check_pvsecret_ebc + + return 0 +} + +check_prereqs_list() { + local tools + + tools=( + "blkid" + "lsblk" + "lsinitrd" + "modprobe" + "mountpoint" + "mount" + "qemu-nbd" + "sha256sum" + "umount" + ) + + for tool in "${tools[@]}"; do + require_command "${tool}" + done + + return 0 +} + +check_prereqs() { + local tools + log_info "checking for required commands" + + # always needed prereqs + tools=( + "grep" + "mkdir" + "uname" + "uuidgen" + "yq" + ) + for tool in "${tools[@]}"; do + require_command "${tool}" + done + + if [[ "${ACTION}" == "convert" ]]; then + check_prereqs_convert + elif [[ "${ACTION}" == "encrypt" ]]; then + check_prereqs_encrypt + elif [[ "${ACTION}" == "full" ]]; then + check_prereqs_convert + check_prereqs_encrypt + elif [[ "${ACTION}" == "list" ]]; then + check_prereqs_list + else + log_error "internal error" + fi + + log_info "all required commands found, continuing..." + + return 0 +} + +require_command() { + local cmd + + cmd="$1" + + command -v "${cmd}" >/dev/null 2>&1 || \ + log_error "${cmd} required but not installed." + + return 0 +} + + +# +# HELPER FUNCTIONS +# + +get_free_nbd() { + local free + + test -b /dev/nbd0 || modprobe nbd + + free=$(lsblk --output NAME,SIZE \ + | grep nbd \ + | grep 0B \ + | grep -oE "nbd[0-9]+" \ + | head -n 1) + + if [[ -z "${free}" ]]; then + log_error "no free NBD found" + fi + + free="/dev/${free}" + + log_info "found free NBD: ${free}" + + NBD="${free}" + + return 0 +} + +execute() { + log_debug "$@" + # shellcheck disable=SC2294 + eval "$@" + + return $? +} + +get_arch() { + ARCH=$(uname -m) + if [[ "${ARCH}" == "" ]]; then + log_error "Architecture not officially supported" + return 1 + else + if [[ "${ARCH}" != "s390x" && "${ARCH}" != "x86_64" ]]; then + log_warn "the detected host architecture ${ARCH} is not one of the officially supported architectures (s390x, x86_64)" + else + log_info "found supported architecture ${ARCH}" + fi + fi + + return 0 +} + + +# $1 path to image +# Returns: Sets global NBD variable +connect_nbd() { + local image i nbd_size + + if [[ $# -lt 1 ]]; then + log_error "internal error" + fi + + image="$1" + + get_free_nbd + + execute qemu-nbd --connect="${NBD}" "${image}" + + log_info "waiting for qemu-nbd:" + for i in {1..10}; do + test -b "${NBD}p1" && break + echo -n "." + sleep 0.1 + done + echo "" + + if [[ -b "${NBD}p1" ]]; then + # Verify the NBD device is actually connected to our image + nbd_size=$(lsblk --bytes --noheadings --output SIZE "${NBD}" 2>/dev/null || echo "0") + if [[ "${nbd_size}" == "0" ]]; then + log_error "NBD device ${NBD} appears to be claimed by another process" + fi + cleanup_push disconnect_nbd + else + log_error "unable to connect ${image} to ${NBD}" + fi + + return 0 +} + +disconnect_nbd() { + test -z "${NBD}" && return 0 + + execute "qemu-nbd --disconnect '${NBD}' 2>/dev/null" + unset NBD + + return 0 +} + +# $1 partition device (e.g., "${NBD}p${BOOT_PARTNUM}") +# $2 mount point path +# $3 optional: mount options (e.g., "-r" for read-only) +mount_partition() { + local partition mount_point mount_opts + + if [[ $# -lt 2 ]]; then + log_error "internal error" + fi + + partition="$1" + mount_point="$2" + mount_opts="${3:-}" + + execute mount "${mount_opts}" "${partition}" "${mount_point}" + + if mountpoint -q "${mount_point}"; then + cleanup_push umount_partition "${mount_point}" + fi + + return 0 +} + +# $1 mountpoint +umount_partition() { + local mntp="${1:?no mountpoint specified}" + + if mountpoint -q "${mntp}"; then + umount "${mntp}" + fi + + return 0 +} + + +# $1 path to asr +# $2 path to toc.pol +add_asr_to_toc() { + local toc asr + + if [[ $# -lt 2 ]]; then + log_error "internal error" + fi + + asr="$1" + toc="$2" + + log_info "adding Add-Secret-Request ${asr} to TOC policy ${toc}" + + xxd -p -s -16 "${asr}" >> "${toc}" + + return 0 +} + +# $1 partition label +# $2 path to image +get_partnum() { + local partition image p partnum + + if [[ $# -lt 2 ]]; then + log_error "internal error" + fi + + partition="$1" + image="$2" + + connect_nbd "${image}" + + # get partitions + for p in "${NBD}"p*; do + + if [[ $(blkid | grep -E "^${p}" | grep -oE "LABEL=\"[a-zA-Z]*\"" | cut -d '"' -f 2) == "${partition}" ]]; then + partnum="$(blkid | grep -oE "^${p}" | cut -d 'p' -f 2)" + log_info "Found partition with label ${partition} at partition ${partnum}" + + cleanup_pop # Disconnect and remove from stack + PARTNUM="${partnum}" + return 0 + fi + done + + cleanup_pop # Disconnect and remove from stack + PARTNUM="" + + return 0 +} + +# Build pv_cmd array for pvimg/pvsecret commands +# $1: hkds - space-separated list of HKD paths +# $2: no_verify - "true" or "false" +# $3: certs - space-separated list of certificate paths (optional if no_verify=true) +# $4: crls - space-separated list of CRL paths (optional if no_verify=true) +# $5: rootca - path to root CA (optional if no_verify=true) +# $6: offline - "true" or "false" (optional if no_verify=true) +# Returns: Sets global pv_cmd array +build_pv_cmd() { + local hkds no_verify certs crls rootca offline + local h c files f + + if [[ $# -lt 2 ]]; then + log_error "internal error: build_pv_cmd requires at least 2 arguments" + fi + + hkds="$1" + no_verify="$2" + certs="${3:-}" + crls="${4:-}" + rootca="${5:-}" + offline="${6:-false}" + + # Initialize pv_cmd array + pv_cmd=() + + # Add HKD files + for h in ${hkds}; do + files=$(ls "${h}") + for f in ${files}; do + pv_cmd+=("--host-key-document" "${f}") + done + done + + # If verification is enabled + if [[ "${no_verify}" == "false" ]]; then + # Add certificates + for c in ${certs}; do + files=$(ls "${c}") + for f in ${files}; do + pv_cmd+=("--cert" "${f}") + done + done + + # Add CRLs + for c in ${crls}; do + files=$(ls "${c}") + for f in ${files}; do + pv_cmd+=("--crl" "${f}") + done + done + + # Add root CA + pv_cmd+=("--root-ca" "${rootca}") + + # Add offline flag if requested + if [[ "${offline}" == "true" ]]; then + pv_cmd+=("--offline") + fi + else + # Verification disabled + pv_cmd+=("--no-verify") + fi + + return 0 +} + +# $1 kernel +# $2 initramfs +# $3 kernel parameter +# $4 output +# $5 yaml configuration +# +# If this function is called by another script that sourced this one the yaml configuration has to +# be supplied as many variables will not be set otherwise +build_sel_image() { + if [[ $# -lt 4 ]]; then + return 1 + fi + + i="$1" + r="$2" + p="$3" + o="$4" + err_noexist "${i}" + err_noexist "${r}" + err_noexist "${p}" + + if [[ $# -gt 4 ]]; then + CONFIG_FILE="$5" + err_noexist "${CONFIG_FILE}" + parse_yaml_pv + fi + + log_info "The following configuration is used to generate the SEL image:" + log_info "kernel: ${i}" + log_info "initramfs: ${r}" + log_info "kernel parameter: $(cat "${p}")" + + # Build pv_cmd array using helper function + build_pv_cmd "${hkds}" "${NO_VERIFY}" "${certs}" "${crls}" "${rootca}" "${offline}" + + # create SEL image + pvimg_cmd=("pvimg" "create") + pvimg_cmd+=("--kernel" "${i}") + pvimg_cmd+=("--ramdisk" "${r}") + pvimg_cmd+=("--parmfile" "${p}") + pvimg_cmd+=("--output" "${o}") + pvimg_cmd+=("${pv_cmd[@]}") + if [[ "${NO_EBC:-false}" != "true" ]]; then + pvimg_cmd+=("--enable-cck-update" "--quiet" "--disable-image-encryption") + fi + pvimg_cmd+=("${PVIMG_USER_OPTS}") + + execute "${pvimg_cmd[@]}" + + return 0 +} + + +# +# USAGE FUNCTIONS +# + +usage() { + local action required_options + + action="${ACTION}" + required_options="--image " + + if [[ "${action}" == "list" ]]; then + echo " +List all available boot loader entries of a s390x guest image and its partitions." + elif [[ "${action}" == "convert" ]]; then + echo " +Convert a s390x guest to a Secure Execution for Linux guest." + elif [[ "${action}" == "encrypt" ]]; then + echo " +Encrypt the root filesystem of a s390x Secure Execution for Linux guest." + elif [[ "${action}" == "" || "${action}" == "full" ]]; then + if [[ "${action}" == "" ]]; then + action="[ACTION]" + fi + echo " +Convert a s390x guest to a Secure Execution for Linux guest with encrypted root +filesystem." + fi + + if [[ "${action}" == "convert" || "${action}" == "encrypt" || "${action}" == "full" ]]; then + required_options="${required_options} --config " + fi + + echo " +Usage: $0 ${action} [OPTIONS] ${required_options}" + + if [[ "${action}" == "[ACTION]" ]]; then + echo " +Actions: + list List boot entries of a guest + convert Convert the guest to Secure Execution for Linux guest + encrypt Encrypt the root filesystem of the guest + full Convert the guest to SEL guest and encrypt the root fs" + fi + + echo " +Options: + -h, --help Show this help text" + if [[ "${action}" == "[ACTION]" ]]; then + echo -e "\t-c, --config\t\tConfiguration yaml file" + fi + + echo "" + + return 0 +} + + +# +# CLI FUNCTIONS +# + +cli() { + local temp + + if [[ "$#" -lt 1 ]]; then + usage + return 1 + fi + + if [[ "$1" == "convert" ]]; then + ACTION="convert" + shift + elif [[ "$1" == "encrypt" ]]; then + ACTION="encrypt" + shift + elif [[ "$1" == "full" ]]; then + ACTION="full" + shift + elif [[ "$1" == "list" ]]; then + ACTION="list" + shift + fi + + temp=$(getopt -o 'hc:i:' --long 'help,config:,image:' -- "$@") + if ! eval set -- "$temp"; then + usage + return 1 + fi + + eval set -- "$temp" + unset temp + + while true; do + case "$1" in + '-h'|'--help') + usage + exit 0 + ;; + '-c'|'--config') + case "$2" in + '') + usage + exit 1 + ;; + *) + CONFIG_FILE="$2" + ;; + esac + shift 2 + continue + ;; + '-i'|'--image') + case "$2" in + '') + usage + exit 1 + ;; + *) + IMAGE="$2" + ;; + esac + shift 2 + continue + ;; + '--') + shift + break + ;; + *) + usage + exit 1 + ;; + esac + done + + if [[ -z ${IMAGE} ]]; then + usage + exit 1 + elif [[ ! -f ${IMAGE} ]]; then + log_error "base image ${IMAGE} does not exist" + + return 1 + fi + + if [[ -z ${CONFIG_FILE} && ${ACTION} != "list" ]]; then + usage + exit 1 + elif [[ -n ${CONFIG_FILE} && ! -f ${CONFIG_FILE} ]]; then + log_error "config file ${CONFIG_FILE} does not exist" + + return 1 + fi + + if [[ ${ACTION} != "convert" && ${ACTION} != "encrypt" && ${ACTION} != "full" && ${ACTION} != "list" ]]; then + usage + exit 1 + fi + + return 0 +} + + +# +# YAML FUNCTIONS +# + +parse_yaml_pv() { + local files f hkd cert crl + + NO_VERIFY=$(yaml_get_opt ".no-verify") + if [[ "${NO_VERIFY}" == "unset" ]]; then + NO_VERIFY=false + fi + + PVIMG_USER_OPTS="$(yaml_get_opt ".convert.pvimg-create-options")" + if [[ "${PVIMG_USER_OPTS}" == "unset" && "${NO_EBC}" != "true" ]]; then + PVIMG_USER_OPTS="--enable-pckmo-hmac" + elif [[ "${PVIMG_USER_OPTS}" == "unset" ]]; then + PVIMG_USER_OPTS="" + fi + + HKDs="$(yaml_get_req ".hkds[]")" + for hkd in ${HKDs}; do + files=$(ls "${hkd}") + for f in ${files}; do + err_noexist "${f}" + done + done + + if [[ "${NO_VERIFY}" == "false" ]]; then + CC_CERTS="$(yaml_get_req ".certificate-chain.certs[]")" + for cert in ${CC_CERTS}; do + files=$(ls "${cert}") + for f in ${files}; do + err_noexist "${f}" + done + done + CC_CRLS="$(yaml_get_req ".certificate-chain.crls[]")" + for crl in ${CC_CRLS}; do + files=$(ls "${crl}") + for f in ${files}; do + err_noexist "${f}" + done + done + CC_ROOTCA=$(yaml_get_req ".certificate-chain.root-ca") + err_noexist "${CC_ROOTCA}" + + CC_OFFLINE=$(yaml_get_opt ".certificate-chain.offline") + if [[ "${CC_OFFLINE}" == "unset" ]]; then + CC_OFFLINE="false" + fi + fi + + return 0 +} + +parse_yaml() { + # general required configuration settings + NO_EBC=$(yaml_get_opt ".no-ebc") + if [[ ${NO_EBC} == "unset" ]]; then + NO_EBC=false + fi + + parse_yaml_pv + + OUT=$(yaml_get_req ".out") + err_noexist "${OUT}" + + if [[ ${ACTION} == "convert" || ${ACTION} == "full" ]]; then + C_BOOT_LOADER_ENTRY=$(yaml_get_req ".convert.boot-loader-entry") + C_SEL_KERNEL_PARAMETER=$(yaml_get_opt ".convert.sel-kernel-parameter") + fi + + if [[ ${ACTION} == "encrypt" || ${ACTION} == "full" ]]; then + E_CCK=$(yaml_get_opt ".encrypt.cck") + if [[ ${E_CCK} != "unset" ]]; then + err_noexist "${E_CCK}" + fi + E_LUKS_RFS_KEY=$(yaml_get_opt ".encrypt.luks-key") + if [[ ${E_LUKS_RFS_KEY} != "unset" ]]; then + err_noexist "${E_LUKS_RFS_KEY}" + fi + E_LUKS_PASSPHRASE=$(yaml_get_opt ".encrypt.luks-passphrase") + if [[ ${E_LUKS_PASSPHRASE} != "unset" ]]; then + err_noexist "${E_LUKS_PASSPHRASE}" + fi + E_LUKS_RFS_KEY_ASR_NAME=$(yaml_get_opt ".encrypt.luks-key-asr-name") + E_LUKS_KEY_SIZE=$(yaml_get_opt ".encrypt.luks-key-size") + if [[ "${E_LUKS_KEY_SIZE}" == "unset" ]]; then + E_LUKS_KEY_SIZE="512" + elif [[ "${E_LUKS_KEY_SIZE}" != "256" && "${E_LUKS_KEY_SIZE}" != "512" ]]; then + log_error "invalid .encrypt.luks-key-size of ${E_LUKS_KEY_SIZE} != 256 or 512" + fi + E_ADD_SECRET_REQUESTS="$(yaml_get_opt ".encrypt.add-secret-requests[]")" + if [[ "${E_ADD_SECRET_REQUESTS}" != "unset" ]]; then + for asr in ${E_ADD_SECRET_REQUESTS}; do + files=$(ls "${asr}") + for f in ${files}; do + err_noexist "${f}" + done + done + + # extension secret is now required + E_EXTENSION_SECRET=$(yaml_get_req ".encrypt.extension-secret") + err_noexist "${E_EXTENSION_SECRET}" + else + E_EXTENSION_SECRET=$(yaml_get_opt ".encrypt.extension-secret") + if [[ ${E_EXTENSION_SECRET} != "unset" ]]; then + err_noexist "${E_EXTENSION_SECRET}" + fi + fi + fi + + return 0 +} + +yaml_get_req() { + yaml_get "$@" true + + return 0 +} + +yaml_get_opt() { + yaml_get "$@" false + + return 0 +} + +yaml_get() { + local key required ret + + if [[ $# -lt 2 ]]; then + log_error "internal error" + fi + + key="$1" + required="$2" + + ret="$(yq e -r "${key} // \"unset\"" "${CONFIG_FILE}")" + + if [[ "${required}" == "true" && "${ret}" == "unset" ]]; then + log_error "key ${key} not specified in ${CONFIG_FILE} but required" + fi + + echo "${ret}" + + return 0 +} + + +# +# ACTION FUNCTIONS +# + +list() { + local image temp_boot ble_dir f title kernel initramfs kernel_params valid + + image="${IMAGE}" + + # mount guest + connect_nbd "${image}" + + temp_boot="${TEMP_DIR}/boot" + mount_partition "${NBD}p${BOOT_PARTNUM}" "${temp_boot}" "-r" + + ble_dir="${temp_boot}/loader/entries" + if [[ ! -d "${ble_dir}" ]]; then + log_error "directory ${ble_dir} does not exist, unable to find boot loader entries" + fi + + log_info "Found the following boot loader entries:" + for f in "${ble_dir}"/*.conf; do + + title="$(grep -E "^title" "${f}" | grep -oE " .*" | grep -oE "[^ ]*")" + kernel="$(grep -E "^linux" "${f}" | cut -d ' ' -f 2)" + initramfs="$(grep -E "^initrd" "${f}" | cut -d ' ' -f 2)" + kernel_params="$(grep -E "^options" "${f}" | cut -d ' ' -f 2-)" + + if echo "${kernel_params}" | grep -q "root=LABEL=root"; then + valid="valid" + elif echo "${kernel_params}" | grep -q "root=/dev/disk/by-label/root"; then + valid="valid" + else + valid="invalid" + fi + + if ! lsinitrd "${TEMP_DIR}${initramfs}" | grep -q "sel-ebc.target"; then + valid="invalid" + fi + + echo " +title: \"${title}\" [ ${valid} ]: + kernel: ${kernel} + SHA256 ($(sha256sum "${TEMP_DIR}${kernel}" | cut -d ' ' -f 1)) + initramfs: ${initramfs} + SHA256 ($(sha256sum "${TEMP_DIR}${initramfs}" | cut -d ' ' -f 1)) + kernel_params: ${kernel_params} +" + done + + cleanup_pop # Unmount temp_boot + cleanup_pop # Disconnect NBD + + return 0 +} + +# Generate SEL guest from existing guest +convert() { + # setup variables + local rootca offline crls certs hkds kernel_parameter + local kernel_parameter_file sel_kernel_parameter initramfs kernel dest + local image temp_boot temp_root sel_header guest_name + local pvimg_cmd pv_cmd ble_array b + + # initialize + image=${IMAGE} + ble=${C_BOOT_LOADER_ENTRY} + kernel_parameter_file="${TEMP_DIR}/kernel_parameter" + hkds=${HKDs} + # optional + sel_kernel_parameter=${C_SEL_KERNEL_PARAMETER} + certs="${CC_CERTS}" + crls="${CC_CRLS}" + rootca="${CC_ROOTCA}" + offline="${CC_OFFLINE}" + + # check for files + err_noexist "${image}" + + if [[ ${sel_kernel_parameter} == "unset" ]]; then + sel_kernel_parameter="" + fi + + # create copy for safety + log_info "copying image; depending on size this may take some time" + execute cp "${image}" "${TEMP_DIR}/image.copy" + image="${TEMP_DIR}/image.copy" + + # mount guest + connect_nbd "${image}" + temp_boot="${TEMP_DIR}/boot" + temp_root="${TEMP_DIR}/root" + mount_partition "${NBD}p${BOOT_PARTNUM}" "${temp_boot}" + mount_partition "${NBD}p${ROOT_PARTNUM}" "${temp_root}" + + # get kernel, initramfs, and kernel parameter from boot loader entry + for f in "${temp_boot}"/loader/entries/*.conf; do + + title="$(grep -E "^title" "${f}" | cut -d ' ' -f 3)" + if [[ "${title}" != "${ble}" ]]; then + continue + fi + + kernel="$(grep -E "^linux" "${f}" | cut -d ' ' -f 2)" + initramfs="$(grep -E "^initrd" "${f}" | cut -d ' ' -f 2)" + if [[ "${NO_EBC}" != "true" ]]; then + kernel_parameter="rd.sel-ebc " + fi + kernel_parameter+="$(grep -E "^options" "${f}" | cut -d ' ' -f 2-) ${sel_kernel_parameter}" + break + done + if [[ "${NO_EBC}" != "true" ]]; then + if [[ $(echo "${kernel_parameter}" | grep "root=LABEL=root") == "" \ + && $? == "1" \ + && $(echo "${kernel_parameter}" | grep "root=/dev/disk/by-label/root") == "" \ + && $? == "1" ]]; then + log_error "root partition has to be identified using the LABEL root in the kernel parameter line" + fi + if [[ $(lsinitrd "${TEMP_DIR}${initramfs}" | grep "/usr/lib/systemd/system/sel-ebc.target") == "" \ + && $? == "1" ]]; then + log_error "initramfs ${initramfs} does not contain IBMs SEL EBC dracut module from s390-tools" + fi + fi + + # check existence + echo "${kernel_parameter}" > "${kernel_parameter_file}" + + build_sel_image "${TEMP_DIR}${kernel}" "${TEMP_DIR}${initramfs}" "${kernel_parameter_file}" "${temp_boot}/sel-ebc.img" + + # extract header from SEL image + sel_header="${OUT}/selhdr.bin" + execute pvextract-hdr -o "${sel_header}" "${temp_boot}/sel-ebc.img" + + # make old boot loader entries invalid (zipl will only find files ending in .conf) + mapfile -t ble_array < <(ls "${temp_boot}/loader/entries/") + for b in "${ble_array[@]}"; do + execute mv "${temp_boot}/loader/entries/${b}" "${temp_boot}/loader/entries/${b}.old" + done + + # create boot entry for SEL + printf 'title sel-ebc\nlinux /boot/sel-ebc.img\n' > "${temp_boot}/loader/entries/sel-ebc.conf" + + # inject systemd service to run zipl + local service_name="ebczipl.service" + local systemd_dir="/usr/lib/systemd/system" + local link + cat > "${temp_root}${systemd_dir}/${service_name}" << EOF +[Unit] +Description=call zipl and shutdown system + +[Service] +Type=oneshot +ExecStart=zipl +ExecStartPost=shutdown now +RemainAfterExit=yes +StandardOutput=file:/var/log/sel-ebc-zipl.log +StandardError=file:/var/log/sel-ebc-zipl.log + +[Install] +WantedBy=multi-user.target +EOF + systemctl --root="${temp_root}" --no-reload enable "${service_name}" + + # unmount mounted image + cleanup_pop # Unmount temp_root + cleanup_pop # Unmount temp_boot + cleanup_pop # Disconnect NBD + + guest_name="sel-ebc-${UUID}" + + # start guest + log_info "starting guest to run zipl" + if [[ "${ARCH}" == "s390x" ]]; then + execute virt-install \ + --name "${guest_name}" \ + --memory 2048 \ + --vcpus 2 \ + --disk path="${image}",format=qcow2 \ + --import \ + --network network=default,model=virtio \ + --graphics none \ + --console pty,target_type=serial \ + --noautoconsole \ + --cpu model=host-model,-deflate \ + --osinfo detect=on,require=off > /dev/null + else + execute qemu-system-s390x \ + -name "${guest_name}" \ + -machine s390-ccw-virtio \ + -cpu max \ + -m 2048 \ + -drive file="${image}",if=virtio,format=qcow2 \ + -serial none \ + -display none \ + -daemonize \ + -netdev user,id=net0 \ + -device virtio-net-ccw,netdev=net0 + fi + + # shutdown and remove guest + if [[ "${ARCH}" == "s390x" ]]; then + log_info "waiting for ${guest_name} to shut down:" + for _ in {1..200}; do + execute virsh list --all | grep "${guest_name}" | grep "shut off" > /dev/null && break + echo -n "." + sleep 0.2 + done + echo "" + + execute virsh undefine "${guest_name}" + else + log_info "waiting for ${guest_name} to shut down:" + for _ in {1..200}; do + execute ! pgrep -af "qemu-system-s390x.*${guest_name}" >/dev/null && break + echo -n "." + sleep 2 + done + echo "" + fi + + # mount guest + connect_nbd "${image}" + temp_root="${TEMP_DIR}/root" + mount_partition "${NBD}p${ROOT_PARTNUM}" "${temp_root}" + + # eject systemd service to run zipl + if [[ -f "${temp_root}${systemd_dir}/${service_name}" ]]; then + rm -f "${temp_root}${systemd_dir}/${service_name}" + fi + link="${temp_root}${systemd_dir}/multi-user.target.wants/${service_name}" + if [[ -h "${link}" ]]; then + rm -f "${link}" + fi + + # unmount mounted image + cleanup_pop # Unmount temp_root + cleanup_pop # Disconnect NBD + + # copy image to output directory + IMAGE="${OUT}/image.qcow2" + execute mv "${image}" "${IMAGE}" + + return 0 +} + +# Encrypt root partition of SEL guest +encrypt() { + local image dest luks_dev_name passphrase luks_segment_key_size segment_key + local asr_name hkds cck asrs certs crls rootca offline extension_secret + local pvsecret_cmd_static pvsecret_cmd pv_cmd + local retr_sec_type luks_segment_key_bytes blob blob_file + local sector_size metadata_size keyslots_size temp_boot sics header + local image_resized root_part retr_sec_id dump key_size pv_out pv_in + local kcmdline files f asr + + image="${IMAGE}" + dest="${OUT}/image.qcow2" + luks_dev_name="cryptroot" + passphrase="${E_LUKS_PASSPHRASE}" + luks_segment_key_size="${E_LUKS_KEY_SIZE}" + segment_key="${E_LUKS_RFS_KEY}" + asr_name="${E_LUKS_RFS_KEY_ASR_NAME}" + hkds="${HKDs}" + cck="${E_CCK}" + asrs="${E_ADD_SECRET_REQUESTS}" + certs="${CC_CERTS}" + crls="${CC_CRLS}" + rootca="${CC_ROOTCA}" + offline="${CC_OFFLINE}" + extension_secret="${E_EXTENSION_SECRET}" + pvsecret_cmd_static="pvsecret create " + + if [[ "${NO_EBC}" == "true" ]]; then + log_info "EBC disabled, skipping encrypt" + return 0 + fi + + # set default LUKS key ASR name + if [[ "${asr_name}" == "unset" ]]; then + asr_name="rfs-luks-key" + fi + + # generate random extension secret if none supplied + if [[ "${extension_secret}" == "unset" ]]; then + extension_secret="${OUT}/extension.secret" + execute dd if=/dev/random of="${extension_secret}" bs=32 count=1 + log_info "generated new extension secret to ${extension_secret}" + fi + + # generate random cck if none supplied + if [[ "${cck}" == "unset" ]]; then + cck="${OUT}/cck.key" + execute dd if=/dev/random of="${cck}" bs=32 count=1 + log_info "generated new Customer-Communication-Key to ${cck}" + fi + + # generate random luks passphrase if none supplied + if [[ "${passphrase}" == "unset" ]]; then + passphrase="${OUT}/passphrase" + execute dd if=/dev/random of="${passphrase}" bs=32 count=1 + log_info "generated new LUKS passphrase to ${passphrase}" + fi + + # check or set default LUKS key size + if [[ "${luks_segment_key_size}" == "unset" ]]; then + if [[ "${segment_key}" == "unset" ]]; then + luks_segment_key_size="512" + else + key_size="$(wc --bytes < "${segment_key}")" + if [[ "${key_size}" -lt "256" ]]; then + log_error "${segment_key} has to be at least 32 bytes" + elif [[ "${key_size}" -lt "512" ]]; then + luks_segment_key_size="256" + else + luks_segment_key_size="256" + fi + fi + fi + + # set retrievable secret type + if [[ "${luks_segment_key_size}" == "256" ]]; then + retr_sec_type="7" + elif [[ "${luks_segment_key_size}" == "512" ]]; then + retr_sec_type="8" + else + log_error "Cipher aes-xts-plain64 requires a key size of 256 or 512 bit!" + fi + + # generate random LUKS key if none supplied + if [[ "${segment_key}" == "unset" ]]; then + luks_segment_key_bytes=$((luks_segment_key_size / 8)) + + segment_key="${OUT}/rfs.key" + execute dd if=/dev/random of="${segment_key}" bs="${luks_segment_key_bytes}" count=1 + log_info "generated new LUKS key for root filesystem to ${segment_key}" + fi + + # Increase QCOW2 disk size by 32MiB to allow additional space for LUKS2 header + log_info "increase size of qcow2 to fit the LUKS header" + execute qemu-img resize "${image}" +32M + # Increase partition size by 32MiB as well, but not the file system, as the LUKS2 encryption is around the filesystem + image_resized="${TEMP_DIR}/resized.qcow2" + execute cp "${image}" "${image_resized}" + execute virt-resize --expand "/dev/vda${ROOT_PARTNUM}" --no-expand-content "${image}" "${image_resized}" + # Remove smaller image, as we already made a copy + execute rm -f "${image}" + image="${image_resized}" + + # move image to output and compress + # the longopt --compress to the short opt -c is broken + execute qemu-img convert --target-format qcow2 -c "${image}" "${dest}" --quiet + execute rm -f "${image}" + image="${dest}" + + # mount guest image + connect_nbd "${image}" + root_part="${NBD}p${ROOT_PARTNUM}" + + # mount boot partition + temp_boot="${TEMP_DIR}/boot" + mount_partition "${NBD}p${BOOT_PARTNUM}" "${temp_boot}" + + # print kernel cmdline + if [[ -f "${temp_boot}/sel-ebc.img" ]]; then + kcmdline="$(strings "${temp_boot}/sel-ebc.img" | grep -E "^rd.sel-ebc")" + + log_info "found the following kernel cmdline:" + log_info "${kcmdline}" + else + log_error "${temp_boot}/sel-ebc.img does not exist, unable to retrieve kernel cmdline" + fi + + # Encrypt the root partition of the mounted guest + log_info "encrypting partition" + execute cryptsetup reencrypt --encrypt --reduce-device-size 32M \ + --cipher aes-xts-plain64 --key-size "${luks_segment_key_size}" \ + --volume-key-file "${segment_key}" --key-file "${passphrase}" "${root_part}" \ + --batch-mode --label=${luks_dev_name} + log_info "done encrypting" + + log_info "reformat LUKS device from aes to paes" + retr_sec_id=$(echo -n "${asr_name}" | sha256sum | cut -d " " -f 1) + + blob="00000000 09000000 000${retr_sec_type}0000 ${retr_sec_id}" + blob_file="$(mktemp)" + echo "${blob}" | xxd -r -p > "${blob_file}" + printf "key blob: \n%s\n" "$(hexdump "${blob_file}")" + if [[ "$(wc -c "${blob_file}" | cut -d " " -f 1 )" != "44" ]]; then + log_error "size of ${blob_file} != 44" + fi + + # parse LUKS header + dump=$(mktemp) + execute cryptsetup luksDump "${root_part}" > "${dump}" + sector_size=$(grep "sector:" "${dump}" | awk 'NR==1{print $2}') + metadata_size=$(grep "Metadata area:" "${dump}" | awk 'NR==1{print $3}') + keyslots_size=$(grep "Keyslots area:" "${dump}" | awk 'NR==1{print $3}') + + # Reformat LUKS header of guest root fs from AES to PAES + execute cryptsetup luksFormat "${root_part}" --batch-mode \ + --key-file "${passphrase}" \ + --uuid "${UUID}" --cipher paes-xts-plain64 \ + --sector-size "${sector_size}" \ + --luks2-metadata-size "${metadata_size}" \ + --luks2-keyslots-size "${keyslots_size}" \ + --volume-key-file "${blob_file}" --key-size 352 \ + --label=${luks_dev_name} + + sics="${temp_boot}/sics" + if [[ ! -d "${sics}" ]]; then + execute mkdir "${sics}" + fi + + # Build pv_cmd array using helper function + build_pv_cmd "${hkds}" "${NO_VERIFY}" "${certs}" "${crls}" "${rootca}" "${offline}" + + # prepare pvsecret command + pvsecret_cmd_static=("pvsecret" "create") + pvsecret_cmd_static+=("${pv_cmd[@]}" "--extension-secret" "${extension_secret}" "--quiet") + header="${TEMP_DIR}/selhdr.bin" + execute pvextract-hdr -o "${header}" "${temp_boot}/sel-ebc.img" + pvsecret_cmd_static+=("--hdr" "${header}") + + # generate ASR for LUKS segment key + pv_in="${segment_key}" + pv_out="${sics}/${asr_name}.asr" + pvsecret_cmd=("${pvsecret_cmd_static[@]}" "--toc-policy" "${sics}/toc.pol" "--output" "${pv_out}" "retrievable" "--secret" "${pv_in}" "--type" "aes-xts" "${asr_name}") + execute "${pvsecret_cmd[@]}" + log_info "generated Add-Secret-Request ${pv_out} from ${pv_in} containing the LUKS encryption key for the root filesystem" + + # generate ASR for CCK + pv_in="${cck}" + pv_out="${sics}/cck.asr" + pvsecret_cmd=("${pvsecret_cmd_static[@]}" "--toc-policy" "${sics}/toc.pol" "--output" "${pv_out}" "update-cck" "--secret" "${pv_in}") + execute "${pvsecret_cmd[@]}" + log_info "generated Add-Secret-Request ${pv_out} from ${pv_in} containing the Customer-Communication-Key" + + # generate ASR for LUKS passphrase + pv_out="${sics}/luks-rfs-passphrase.asr" + pv_in="${passphrase}" + pvsecret_cmd=("${pvsecret_cmd_static[@]}" "--toc-policy" "${sics}/toc.pol" "--output" "${pv_out}" "retrievable" "--secret" "${pv_in}" "--type" "plain" "luks-rfs-passphrase") + execute "${pvsecret_cmd[@]}" + log_info "generated Add-Secret-Request ${pv_out} from ${pv_in} containing the LUKS passphrase for the root filesystem" + + # add additional ASRs to toc.pol + if [[ "${asrs}" != "unset" ]]; then + for asr in ${asrs}; do + files=$(ls "${asr}") + for f in ${files}; do + add_asr_to_toc "${f}" "${sics}/toc.pol" + execute cp "${f}" "${sics}/" + log_info "added user provided Add-Secret-Request ${f}" + done + done + fi + + # generate toc.asr + pvsecret_cmd=("${pvsecret_cmd_static[@]}" "--output" "${sics}/toc.asr" "--policy" "toc.pol" "meta") + execute cp "${sics}/toc.pol" "toc.pol" + execute "${pvsecret_cmd[@]}" + execute rm -f toc.pol + + # check sanity of SICS + pv_cmd=("pvebc" "--dry-run" "--toc" "${sics}/toc.asr") + execute "${pv_cmd[@]}" + log_info "${sics} is sane" + + # unmount image + cleanup_pop # Unmount temp_boot + cleanup_pop # Disconnect NBD + + return 0 +} + + +# +# MAIN +# + +main() { + # Set strict error handling when running as script + local - + set -Eu + + # call cleanup on error + # PIPE is needed to allow out=$(main list -i | grep -m 1 ) + # bash will close the subshell executing the main function as soon as grep has found a match + # cleanup will not be executed anymore + trap 'cleanup 1' ERR EXIT PIPE + + # general + ACTION="" + NBD="" + ARCH="" + CONFIG_FILE="" + OUT="" + UUID="" + TEMP_DIR="" + NO_VERIFY="" + NO_EBC="" + BOOT_PARTNUM="" + ROOT_PARTNUM="" + HKDs="" + IMAGE="" + PARTNUM="" + + # certificate chain related + CC_CERTS="" + CC_CRLS="" + CC_ROOTCA="" + CC_OFFLINE="" + + # conversion related + C_BOOT_LOADER_ENTRY="" + C_SEL_KERNEL_PARAMETER="" + + # encryption related + E_CCK="" + E_EXTENSION_SECRET="" + E_LUKS_RFS_KEY="" + E_LUKS_PASSPHRASE="" + E_LUKS_RFS_KEY_ASR_NAME="" + E_LUKS_KEY_SIZE="" + E_ADD_SECRET_REQUESTS="" + + # parse command line + cli "$@" + + # set up logger + setup_logger + + # get architecture + get_arch + + # check for prereqs + check_prereqs + + if [[ "${ACTION}" != "list" ]]; then + # parse yaml configuration + parse_yaml + + # setup output directory + if [[ ! -d ${OUT} ]]; then + execute mkdir "${OUT}" + if [[ ! -d ${OUT} ]]; then + log_error "unable to create ${OUT}" + fi + fi + fi + + # get partitions + get_partnum "root" "${IMAGE}" + ROOT_PARTNUM="${PARTNUM}" + test -z "${ROOT_PARTNUM}" && log_error "unable to find root partition on ${IMAGE}" + get_partnum "boot" "${IMAGE}" + BOOT_PARTNUM="${PARTNUM}" + test -z "${BOOT_PARTNUM}" && log_error "unable to find boot partition on ${IMAGE}" + + # setup temporary directory + UUID=$(uuidgen) + TEMP_DIR=/opt/sel-ebc-${UUID} + execute mkdir "${TEMP_DIR}" + if [[ ! -d ${TEMP_DIR} ]]; then + log_error "unable to create ${TEMP_DIR}" + fi + # Register cleanup for TEMP_DIR (will be removed on any exit) + if [[ -d "${TEMP_DIR}" ]]; then + cleanup_push rm -rf "${TEMP_DIR}" + fi + execute mkdir "${TEMP_DIR}/boot" + execute mkdir "${TEMP_DIR}/root" + + log_info "all temporary resources will be placed in ${TEMP_DIR}" + + if [[ "${ACTION}" == "list" ]]; then + list + fi + + if [[ "${ACTION}" == "convert" || "${ACTION}" == "full" ]]; then + convert + fi + + if [[ "${ACTION}" == "encrypt" || "${ACTION}" == "full" ]]; then + encrypt + fi + + # Done - run cleanup explicitly to support sourcing + log_info "done" + + # Run cleanup stack to reset logging and clean up resources + # This ensures cleanup happens even when script is sourced + cleanup 0 + return $? +} + + +# +# SOURCE WRAPPER +# + +if [[ "$0" == "${BASH_SOURCE[0]}" ]]; then + main "${@}" +fi diff --git a/scripts/pvics.8 b/scripts/pvics.8 new file mode 100644 index 00000000..55ec9a97 --- /dev/null +++ b/scripts/pvics.8 @@ -0,0 +1,340 @@ +.\" Copyright IBM Corp. +.\" s390-tools is free software; you can redistribute it and/or modify +.\" it under the terms of the MIT license. See LICENSE for details. +.\" +.TH PVICS 8 "April 2026" "s390-tools" + +.SH NAME +pvics \- Convert qcow2 KVM guest images to EBC-ready SEL images + +.SH SYNOPSIS +.B pvics +.I ACTION +.RB [ \-h | \-\-help ] +.RB [ \-c | \-\-config +.IR CONFIG_FILE ] +.RB [ \-i | \-\-image +.IR BASE_IMAGE ] + +.SH DESCRIPTION +Use the \fBpvics\fR tool to convert existing QEMU Copy/-On/-Write version 2(qcow2) KVM +guest images into images that are ready for Early Boot Customization (EBC) and Secure Execution for Linux (SEL). +The tool encrypts the root file system and prepares all resources required for EBC. + +The tool operates on a copy of the original base image and performs operations +in three main phases: +.IP \(bu 2 +Retrieving information about a given base image +.IP \(bu 2 +Converting a base image according to a configuration +.IP \(bu 2 +Encrypting the root file system and preparing EBC resources + +All operations preserve the original base image. Logs are written to a +temporary file, with the filename logged as the first message during runtime. +Temporary artifacts are automatically cleaned up on completion or failure. + +.SH ACTIONS +.TP +.B list +Retrieve information about a given base image. This action displays available +boot loader entries from \fB/boot/loader/entries\fR, along with kernel and +initramfs hashes and the kernel command line. This is useful for: +.RS +.IP \(bu 2 +Determining valid values for the \fBboot-loader-entry\fR configuration option +.IP \(bu 2 +Comparing components between base and converted images +.IP \(bu 2 +Verifying image contents before conversion +.RE + +.TP +.B convert +Convert a base image according to the configuration file. This action: +.RS +.IP \(bu 2 +Fetches kernel, initramfs, and kernel command line from the specified boot +loader entry +.IP \(bu 2 +Builds a SEL image using \fBpvimg\fR(1) +.IP \(bu 2 +Updates \fB/boot/bootmap\fR to boot into the SEL image +.RE +.IP +The resulting image will be a SEL guest image. It requires encryption to be +fully EBC-ready (unless \fBno-ebc\fR is enabled in the configuration). + +.TP +.B encrypt +Encrypt the root file system and prepare EBC resources. This action: +.RS +.IP \(bu 2 +Generates secure defaults (CCK, extension secret, LUKS keys) if not provided +.IP \(bu 2 +Encrypts the root file system using LUKS with PAES +.IP \(bu 2 +Populates \fB/boot/sics/\fR with add/-secret requests +.IP \(bu 2 +Creates \fBtoc.pol\fR and \fBtoc.asr\fR for integrity protection +.RE +.IP +This action requires EBC. The guest can boot only by using PAES to open the root file system. + +.TP +.B full +Perform both \fBconvert\fR and \fBencrypt\fR actions in sequence. This is +equivalent to running \fBconvert\fR followed by \fBencrypt\fR, but in a single +invocation. + +.SH ARGUMENTS +.TP +.I ACTION +The action to perform: \fBlist\fR, \fBconvert\fR, \fBencrypt\fR, or \fBfull\fR. + +.SH OPTIONS +.TP +.BR \-h ", " \-\-help +Display help message and exit. + +.TP +.BR \-c ", " \-\-config " " \fICONFIG_FILE\fR +Path to the YAML configuration file. See \fBpvics.yaml\fR(5) for the +configuration file format and options. +.br +Required for \fBconvert\fR, \fBencrypt\fR, and \fBfull\fR actions. +.br +Not required for the \fBlist\fR action. + +.TP +.BR \-i ", " \-\-image " " \fIBASE_IMAGE\fR +Path to the base qcow2 image file to process. Required for all actions. + +.SH CONVERSION PROCESS +The conversion process consists of three phases: + +.SS Component Fetching +The tool retrieves the kernel, initramfs, and kernel command line from the +boot loader entry specified in the configuration file. The components are +validated to ensure they meet SEL EBC requirements. + +The tool prepends \fBrd.sel-ebc\fR to the kernel command line to trigger the +SEL EBC dracut module (unless \fBno-ebc\fR is enabled) and appends any +user-provided kernel parameters from the configuration. + +.SS SEL Image Build +The SEL image is built using \fBpvimg\fR(1) with the fetched components. +Additional options may be specified in the configuration file via +\fB.convert.pvimg-create-options\fR. + +.SS Bootmap Update +The tool updates the bootmap to boot into the new SEL image: +.IP \(bu 2 +Existing boot loader entries in \fB/boot/loader/entries/*.conf\fR are renamed +to \fB*.conf.old\fR +.IP \(bu 2 +A new entry \fBsel-ebc.conf\fR is created that points to \fB/boot/sel-ebc.img\fR +.IP \(bu 2 +\fBzipl\fR(8) is run to update the bootmap. + +The guest is temporarily started to update the bootmap using libvirt on z/Architecture +or qemu on non-z/Architecture architectures. Failures are logged to +\fB/var/log/sel-ebc-zipl.log\fR. + +.SH ENCRYPTION PROCESS +The encryption process consists of three phases: + +.SS Secure Default Generation +If not provided in the configuration, the following are generated from +\fB/dev/random\fR: +.IP \(bu 2 +Customer communication key (CCK) +.IP \(bu 2 +Root file system LUKS encryption key +.IP \(bu 2 +Extension secret for add/-secret requests +.IP \(bu 2 +LUKS passphrase + +.SS Root File System Encryption +The root file system is encrypted using the following steps: +.IP 1. 3 +Resize the qcow2 image to accommodate the LUKS header +.IP 2. 3 +Resize the root partition (but not the file system) to fit the LUKS header +.IP 3. 3 +Encrypt the root file system using LUKS +.IP 4. 3 +Reformat the LUKS header from AES to PAES (Protected AES) + +.SS AES to PAES Conversion +The LUKS header is converted from standard AES encryption to protected AES (PAES). +With PAES, encryption secrets are stored in the ultravisor secret store rather than directly in the LUKS header. + +.SS SICS Population +The SEL Image Customization Source directory (\fB/boot/sics\fR) is populated +with the following secrets as add/-secret requests: +.IP \(bu 2 +Customer communication key (CCK) +.IP \(bu 2 +LUKS encryption key +.IP \(bu 2 +LUKS passphrase +.IP \(bu 2 +Any additional add/-secret requests specified in the configuration + +All automatically built add/-secret requests are added to \fBtoc.pol\fR, which verifies the +completeness of \fB/boot/sics\fR. User-supplied add/-secret requests from the configuration +are also added. Finally, \fBtoc.asr\fR is built as a meta secret that links +to \fBtoc.pol\fR for integrity protection. + +.SH BUILDING SEL IMAGES WITHOUT EBC +While the primary purpose of \fBpvics\fR is to create EBC-ready SEL images with +encrypted root file systems, it can also build SEL images without EBC +functionality using the \fBno-ebc\fR configuration option. + +When \fBno-ebc: true\fR is set in the configuration: +.IP \(bu 2 +The \fBconvert\fR action builds a SEL image without EBC-specific flags +.IP \(bu 2 +The \fBencrypt\fR action is completely skipped +.IP \(bu 2 +The \fBfull\fR action becomes equivalent to \fBconvert\fR only +.IP \(bu 2 +The \fBrd.sel-ebc\fR kernel parameter is not added +.IP \(bu 2 +\fB--enable-cck-update\fR and \fB--disable-image-encryption\fR flags are not +passed to \fBpvimg\fR + +This mode is useful for: +.IP \(bu 2 +Single-party image creation where the same entity performs conversion, +customization, and encryption +.IP \(bu 2 +Testing SEL guest functionality without EBC complexity +.IP \(bu 2 +Development and debugging scenarios +.IP \(bu 2 +Simplified SEL image generation workflows + +.SH OUTPUT FILES +All output files are written to the directory specified by the \fBout\fR +configuration option: + +.TP +\fB/image.qcow2\fR +The converted SEL image (after \fBconvert\fR or \fBfull\fR action) + +.TP +\fB/cck.key\fR +Customer communication key (generated if not provided) + +.TP +\fB/extension.secret\fR +Extension secret for add/-secret requests (generated if not provided) + +.TP +\fB/rfs.key\fR +Root file system LUKS encryption key (generated if not provided) + +.TP +\fB/passphrase\fR +LUKS passphrase (generated if not provided) + +.TP +\fB/*.asr\fR +Generated add-secret request files + +.SH TEMPORARY FILES +.TP +\fB/tmp/tmp.*\fR +Log file (filename logged at startup) + +.SH EXIT STATUS +.TP +.B 0 +Success +.TP +.B 1 +General error (invalid arguments, missing files, operation failure) + +.SH EXAMPLES +.SS List Boot Loader Entries +.nf +pvics list \-\-image /path/to/base-image.qcow2 +.fi + +.SS Convert Image Only +.nf +pvics convert \-\-config /path/to/config.yaml \-\-image /path/to/base-image.qcow2 +.fi + +.SS Encrypt Image Only +.nf +pvics encrypt \-\-config /path/to/config.yaml \-\-image /path/to/converted-image.qcow2 +.fi + +.SS Full Conversion and Encryption +.nf +pvics full \-\-config /path/to/config.yaml \-\-image /path/to/base-image.qcow2 +.fi + +.SS Build SEL Image Without EBC +.nf +# config.yaml contains: no-ebc: true +pvics convert \-\-config /path/to/config.yaml \-\-image /path/to/base-image.qcow2 +.fi + +.SH FILES +.TP +\fB/boot/loader/entries/*.conf\fR +Boot loader entry files in the base image + +.TP +\fB/boot/sel-ebc.img\fR +The resulting SEL image file in the guest + +.TP +\fB/boot/sics/\fR +SEL image-customization source directory that contains EBC resources + +.TP +\fB/boot/sics/toc.pol\fR +Table-of-contents policy file that lists all add/-secret requests + +.TP +\fB/boot/sics/toc.asr\fR +Meta secret for integrity protection of toc.pol + +.TP +\fB/var/log/sel-ebc-zipl.log\fR +Log file for zipl bootmap update operations + +.SH NOTES +.IP \(bu 2 +All operations are performed on a copy of the original base image. +.IP \(bu 2 +The tool requires root privileges for file-system operations. +.IP \(bu 2 +The LUKS device is named \fBcryptroot\fR. +.IP \(bu 2 +The temporary working directory is created at \fB/opt/sel-\fR. +.IP \(bu 2 +The \fBno-verify\fR option should not be used in production environments. +.IP \(bu 2 +Generated secrets are written to the output directory and should be secured +appropriately. + +.SH SEE ALSO +.BR pvics.yaml (5), +.BR pvsecret (1), +.BR pvimg (1), +.BR pvebc (8), +.BR zipl (8), +.BR cryptsetup (8), +.BR lsinitrd (1) +.PP +Linux on IBM Z and IBM LinuxONE: Secure Execution for Linux documentation + +.SH AUTHOR +IBM Corporation diff --git a/scripts/pvics.yaml b/scripts/pvics.yaml new file mode 100644 index 00000000..fb73edbe --- /dev/null +++ b/scripts/pvics.yaml @@ -0,0 +1,106 @@ +# SPDX-License-Identifier: MIT +# +# Copyright IBM Corp. + + +# OPTIONAL (default: false) +no-verify: false +# This option controls whether the --no-verify flag is used with the pv commands. + +# OPTIONAL (default: false) +no-ebc: false +# This option controls whether to trigger the SEL EBC related systemd units. +# It can be used to create a SEL guest image from an existing qcow2 image without utilizing SEL EBC. +# Setting this to true will prevent the encrypt action from running. +# In this case, the full action equals the convert action. +# Setting this to true will prevent the default pvimg options (see below) from being added and instead +# only add the user provided ones in .convert.pvimg-options. + +# REQUIRED +out: data/output +# This specifies the local directory used for output. + +# REQUIRED +hkds: + - data/*.hkd +# These are the paths to all relevant Host-Key-Documents. +# The paths may contain wildcard patterns. + +# REQUIRED if .no-verify is false +# UNUSED if .no-verify is true +# This section contains all relevant files for verification of the certificate chain. +# See man pvsecret for more detailed information about any of the keys in this section. +certificate-chain: + # The paths may contain wildcard patterns. + certs: + - data/*.cert + + # The paths may contain wildcard patterns. + crls: + - data/*.crl + + # This is the pvsecret create offline option. + offline: true + + # This is the root CA of the given certificates. + root-ca: data/root.ca + +# This section contains relevant information for the first phase: conversion. +convert: + # REQUIRED + boot-loader-entry: boot_loader_entry_title + # This is the boot loader entry name to be used for kernel, initramfs and kernel parameter. + # You can list available boot loader entries with the list action. + + # OPTIONAL + sel-kernel-parameter: swiotlb=524288 + # These are additional kernel parameters for the resulting SEL image. + # The parameter rd.sel-ebc will always be added. + + # OPTIONAL (default: --enable-pckmo-hmac) + pvimg-create-options: --enable-pckmo-hmac + # These are additional pvimg options for the resulting SEL image. + # The options --disable-image-encryption and --enable-update-cck are always used except when .no-ebc is true. + +# This section contains relevant information for the second phase: encryption. +encrypt: + # OPTIONAL + cck: data/cck.key + # This is the local path to the CCK. + # If none is supplied, one will be generated and written to .out/cck.key. + + # OPTIONAL + extension-secret: data/extension.secret + # This is the local path to the extension secret for pvsecret create. + # This is REQUIRED if .encrypt.add-secret-requests is used. + # In this case, the extension secret must be the one from the supplied ASRs. + # If none is supplied, one will be generated and written to .out/extension.secret. + + # OPTIONAL + luks-key: data/rfs.key + # This is the local path to the LUKS encryption key. + # If none is supplied, one will be generated and written to .out/rfs.key. + + # OPTIONAL + luks-passphrase: data/passphrase + # This is the local path to a file containing the LUKS passphrase. + # If none is supplied, one will be generated and written to .out/passphrase. + + # OPTIONAL (default: rfs-luks-key) + luks-key-asr-name: aes-xts-segment-key + # This is the name of the ASR containing the LUKS encryption key. + + # OPTIONAL (default: depending on .encrypt.luks-key) + luks-key-size: 512 + # This is the key size of the LUKS encryption key (256 or 512). + # If .encrypt.luks-key is supplied, this will default to a size depending on the key. + # Otherwise, this will default to 512 and generate a matching key. + # If this is supplied but no key is supplied, a key of the requested size will be generated. + + # OPTIONAL + add-secret-requests: + - data/*.asr + # These are additional secrets to be added during boot. + # Using this will make .encrypt.extension-secret a required argument. + # The supplied extension secret and the extension secret of supplied ASRs must be the same. + # The paths may contain wildcard patterns. \ No newline at end of file diff --git a/scripts/pvics.yaml.5 b/scripts/pvics.yaml.5 new file mode 100644 index 00000000..1950eaec --- /dev/null +++ b/scripts/pvics.yaml.5 @@ -0,0 +1,321 @@ +.\" Copyright IBM Corp. +.\" s390-tools is free software; you can redistribute it and/or modify +.\" it under the terms of the MIT license. See LICENSE for details. +.\" +.TH PVICS.YAML 5 "April 2026" "s390-tools" + +.SH NAME +pvics.yaml \- Configuration file for pvics SEL EBC image conversion tool + +.SH DESCRIPTION +The \fBpvics.yaml\fR file is a YAML-formatted configuration file used by the +\fBpvics\fR tool to control the conversion of existing qcow2 KVM guest images +to EBC-ready Secure Execution for Linux (SEL) images. The configuration file +specifies parameters for image conversion, root file system encryption, and +early boot customization (EBC) resource preparation. + +See \fBpvics\fR(8) for detailed information about the tool's actions and the +conversion process. + +.SH FILE FORMAT +The configuration file uses YAML syntax with the following top-level sections: +.TP +\fBGlobal Options\fR +General settings that apply to all actions +.TP +\fBCertificate Chain\fR +Certificate verification configuration +.TP +\fBConversion Options\fR +Settings specific to the \fBconvert\fR action +.TP +\fBEncryption Options\fR +Settings specific to the \fBencrypt\fR action + +.SH GLOBAL OPTIONS +.TP +\fBno-verify:\fR \fIboolean\fR +Disables certificate chain verification for pv commands when set to true. +Default: \fBfalse\fR. +.br +\fBWARNING:\fR Do not disable certificate chain verification in production environments. + +.TP +\fBno-ebc:\fR \fIboolean\fR +Controls whether EBC functionality is used. When set to +\fBtrue\fR, a SEL guest image is created without using EBC. +Default: \fBfalse\fR. +.br +When enabled: +.RS +.IP \(bu 2 +The \fBencrypt\fR action is skipped +.IP \(bu 2 +The \fBfull\fR action behaves the same as the \fBconvert\fR action +.IP \(bu 2 +Default \fBpvimg\fR options are not added; only user-provided options specified in +\fB.convert.pvimg-create-options\fR are used +.IP \(bu 2 +The \fBrd.sel-ebc\fR kernel parameter is not added +.RE + +.TP +\fBout:\fR \fIpath\fR +\fB(REQUIRED)\fR Local directory path used for output files. All generated +files (converted images, keys, ASRs) will be written to this directory. + +.TP +\fBhkds:\fR \fIlist\fR +\fB(REQUIRED)\fR List of paths to host-key-documents (HKDs). Paths may contain +wildcard patterns, for example \fBdata/*.hkd\fR. +.br +Example: +.RS +.nf +hkds: + - data/*.hkd + - /path/to/specific.hkd +.fi +.RE + +.SH CERTIFICATE CHAIN +The \fBcertificate-chain\fR section contains all relevant files for verification +of the certificate chain. This section is \fBREQUIRED\fR if \fBno-verify\fR is +\fBfalse\fR, and \fBUNUSED\fR if \fBno-verify\fR is \fBtrue\fR. + +See \fBpvsecret\fR(1) for more detailed information about certificate chain +verification. + +.TP +\fBcertificate-chain.certs:\fR \fIlist\fR +List of paths to certificate files. Paths may contain wildcard patterns. +.br +Example: +.RS +.nf +certs: + - data/*.cert +.fi +.RE + +.TP +\fBcertificate-chain.crls:\fR \fIlist\fR +List of paths to certificate revocation list (CRL) files. Paths may contain +wildcard patterns. +.br +Example: +.RS +.nf +crls: + - data/*.crl +.fi +.RE + +.TP +\fBcertificate-chain.offline:\fR \fIboolean\fR +Enables offline mode for the \fBpvsecret create\fR command. When \fBtrue\fR, no +network access is attempted for certificate verification. + +.TP +\fBcertificate-chain.root-ca:\fR \fIpath\fR +Path to the root Certificate Authority (CA) file for the certificate chain. + +.SH CONVERSION OPTIONS +The \fBconvert\fR section contains configuration options for the image +conversion phase. These options are used only when running the \fBconvert\fR or +\fBfull\fR actions and have no effect on other actions. + +.TP +\fBconvert.boot-loader-entry:\fR \fIstring\fR +\fB(REQUIRED)\fR Title of an existing boot loader entry from the base image. +This entry specifies which kernel, initramfs, and kernel command line to use +for the SEL image. +.br +Use the \fBlist\fR action to display available boot loader entries in the +base image. + +.TP +\fBconvert.sel-kernel-parameter:\fR \fIstring\fR +\fB(OPTIONAL)\fR Additional kernel parameters to append to the kernel command +line of the resulting SEL image. The \fBrd.sel-ebc\fR parameter is always +prepended automatically (unless \fBno-ebc\fR is \fBtrue\fR). +.br +Example: +.RS +.nf +sel-kernel-parameter: swiotlb=524288 +.fi +.RE + +.TP +\fBconvert.pvimg-create-options:\fR \fIstring\fR +\fB(OPTIONAL)\fR Additional options to pass to the \fBpvimg\fR command during +SEL image creation. +.br +See \fBpvimg\fR(1) for available options. + +.SH ENCRYPTION OPTIONS +The \fBencrypt\fR section contains configuration options specific to the root filesystem +encryption and EBC resource preparation phase. These options are only used when running +the \fBencrypt\fR or \fBfull\fR actions and are ignored when \fBno-ebc\fR +is \fBtrue\fR. + +.TP +\fBencrypt.cck:\fR \fIpath\fR +\fB(OPTIONAL)\fR Path to the customer communication key (CCK) file. If not +supplied, a CCK will be generated from \fB/dev/random\fR and written to +\fB/cck.key\fR. + +.TP +\fBencrypt.extension-secret:\fR \fIpath\fR +\fB(OPTIONAL)\fR Path to the extension secret file used for \fBpvsecret create\fR +commands. If not supplied, an extension secret will be generated from +\fB/dev/random\fR and written to \fB/extension.secret\fR. +.br +\fBREQUIRED\fR if \fBencrypt.add-secret-requests\fR is used. In this case, the +extension secret must match the one used in the supplied add/-secret requests. + +.TP +\fBencrypt.luks-key:\fR \fIpath\fR +\fB(OPTIONAL)\fR Path to the LUKS encryption key file for the root filesystem. +If not supplied, a key will be generated from \fB/dev/random\fR and written to +\fB/rfs.key\fR. + +.TP +\fBencrypt.luks-passphrase:\fR \fIpath\fR +\fB(OPTIONAL)\fR Path to a file that contains the LUKS passphrase. If not supplied, +a passphrase will be generated from \fB/dev/random\fR and written to +\fB/passphrase\fR. + +.TP +\fBencrypt.luks-key-asr-name:\fR \fIstring\fR +\fB(OPTIONAL)\fR Name of the add-secret request that contains the LUKS +encryption key. Default: \fBrfs-luks-key\fR. + +.TP +\fBencrypt.luks-key-size:\fR \fIinteger\fR +\fB(OPTIONAL)\fR Key size in bits for the LUKS encryption key. Valid values: +\fB256\fR or \fB512\fR. If not specified, the size is determined from the +supplied key file or a default size is used for generated keys. + +.TP +\fBencrypt.add-secret-requests:\fR \fIlist\fR +\fB(OPTIONAL)\fR List of paths to additional add/-secret request files to +be added during boot. Paths may contain wildcard patterns. +.br +When using this option: +.RS +.IP \(bu 2 +\fBencrypt.extension-secret\fR is \fBREQUIRED\fR +.IP \(bu 2 +The supplied extension secret must match the extension secret used in all +supplied add/-secret requests +.RE +.br +Example: +.RS +.nf +add-secret-requests: + - data/*.asr + - /path/to/custom.asr +.fi +.RE + +.SH EXAMPLES +.SS Minimal Configuration for EBC +.nf +out: /path/to/output +hkds: + - /path/to/*.hkd +certificate-chain: + certs: + - /path/to/*.cert + crls: + - /path/to/*.crl + offline: true + root-ca: /path/to/root.ca +convert: + boot-loader-entry: "My Boot Entry" +.fi + +.SS Configuration with Custom Encryption Keys +.nf +out: /path/to/output +hkds: + - /path/to/*.hkd +certificate-chain: + certs: + - /path/to/*.cert + crls: + - /path/to/*.crl + offline: true + root-ca: /path/to/root.ca +convert: + boot-loader-entry: "My Boot Entry" + sel-kernel-parameter: swiotlb=524288 + pvimg-create-options: --enable-pckmo-hmac +encrypt: + cck: /path/to/cck.key + luks-key: /path/to/rfs.key + luks-passphrase: /path/to/passphrase + luks-key-size: 512 +.fi + +.SS Configuration with Additional ASRs +.nf +out: /path/to/output +hkds: + - /path/to/*.hkd +certificate-chain: + certs: + - /path/to/*.cert + crls: + - /path/to/*.crl + offline: true + root-ca: /path/to/root.ca +convert: + boot-loader-entry: "My Boot Entry" +encrypt: + extension-secret: /path/to/extension.secret + add-secret-requests: + - /path/to/*.asr +.fi + +.SS SEL Image Without EBC +.nf +no-ebc: true +out: /path/to/output +hkds: + - /path/to/*.hkd +convert: + boot-loader-entry: "My Boot Entry" +.fi + +.SH FILES +.TP +\fB/boot/loader/entries/*.conf\fR +Boot loader entry files in the base image +.TP +\fB/boot/sel-ebc.img\fR +The resulting SEL image file +.TP +\fB/boot/sics/\fR +SEL Image customization source directory containing EBC resources +.TP +\fB/boot/sics/toc.pol\fR +Table of contents policy file listing all add/-secret requests +.TP +\fB/boot/sics/toc.asr\fR +Meta-secret for integrity protection of toc.pol +.TP +\fB/var/log/sel-ebc-zipl.log\fR +Log file for zipl bootmap update operations + +.SH SEE ALSO +.BR pvics (8), +.BR pvsecret (1), +.BR pvimg (1), +.BR zipl (8), +.BR cryptsetup (8) +.PP +Linux on IBM Z and IBM LinuxONE: Secure Execution for Linux documentation