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