Files
s390-tools/scripts/pvics
Finn Callies cf45a6e108 pvics: Fail on non-existend BLE
Issue an error if the specified boot loader entry cannot be found.

Reviewed-by: Holger Dengler <dengler@linux.ibm.com>
Signed-off-by: Finn Callies <fcallies@linux.ibm.com>
Signed-off-by: Jan Höppner <hoeppner@linux.ibm.com>
2026-07-20 15:25:57 +02:00

1596 lines
35 KiB
Bash
Executable File

#!/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 <FILE>"
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 <FILE>"
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
--version Show version information"
if [[ "${action}" == "[ACTION]" ]]; then
echo -e "\t-c, --config\t\tConfiguration yaml file"
fi
echo ""
return 0
}
print_version() {
cat <<-EOD
$0: version %S390_TOOLS_VERSION%
Copyright IBM Corp.
EOD
}
#
# 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,version,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
;;
'--version')
print_version
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
kernel_parameter=""
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 [[ -z "${kernel_parameter}" ]]; then
log_error "No boot loader entry with title \"${ble}\" found"
fi
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 env -S LIBGUESTFS_BACKEND=direct 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 <image> | grep -m 1 <filter>)
# 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