#!/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 --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 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