From 7c808479be2895c3f9fb63f39b3516985de076d5 Mon Sep 17 00:00:00 2001 From: Vince van Oosten Date: Tue, 24 May 2022 21:40:54 +0200 Subject: [PATCH 01/20] add files --- src/zfs/clevis-zfs-bind | 107 +++++++++++++++++++++++++++++ src/zfs/clevis-zfs-common | 140 ++++++++++++++++++++++++++++++++++++++ src/zfs/clevis-zfs-list | 33 +++++++++ src/zfs/clevis-zfs-unbind | 60 ++++++++++++++++ src/zfs/clevis-zfs-unlock | 66 ++++++++++++++++++ 5 files changed, 406 insertions(+) create mode 100755 src/zfs/clevis-zfs-bind create mode 100755 src/zfs/clevis-zfs-common create mode 100755 src/zfs/clevis-zfs-list create mode 100755 src/zfs/clevis-zfs-unbind create mode 100755 src/zfs/clevis-zfs-unlock diff --git a/src/zfs/clevis-zfs-bind b/src/zfs/clevis-zfs-bind new file mode 100755 index 00000000..df957956 --- /dev/null +++ b/src/zfs/clevis-zfs-bind @@ -0,0 +1,107 @@ +#!/bin/bash +set -euo pipefail + +. clevis-zfs-common + +SUMMARY="Binds a ZFS dataset using the specified policy" + + + +function usage() { + cat >&2 <<-USAGE_END + Usage: clevis zfs bind [-f] [-k KEY] -d DATASET PIN CFG + + $SUMMARY: + + -f Do not prompt when overwriting configuration + + -d DATASET The zfs dataset on which to perform binding + + -k KEY Non-interactively read zfs password from KEY file + -k - Non-interactively read zfs password from standard input + + USAGE_END +} + + +function bind_zfs_dataset() { + local dataset="${1}" + local pin="${2}" + local cfg="${3}" + local key="${4}" + local overwrite="${5:-}" + + local existing_key clevis_data + + if [[ -z "${overwrite}" ]] && zfs_is_bound "${dataset}"; then + error "given dataset already has a clevis binding, not overwriting: ${dataset}." + fi + + existing_key="$(load_key "${dataset}" "${key}")" + + if ! zfs_test_key "${dataset}" <<<"${existing_key}"; then + error "given key does not unlock ${dataset}" + fi + + echo >&2 -n 'creating new clevis data... ' + clevis_data="$(clevis encrypt "${pin}" "${cfg}" <<<"${existing_key}" )" + echo >&2 'ok' + + [[ -n "${overwrite}" ]] && zfs_wipe_clevis_data "${dataset}" && echo >&2 'wiped old clevis data' + + zfs_bind_clevis_data "${dataset}" "${clevis_data}" +} + +function check_valid_dataset() { + local dataset="${1}" + + if ! zfs_get_prop "${dataset}" 'name' -snone &>/dev/null; then + error "${dataset} is not a zfs dataset!" + fi + + if ! zfs_is_encryptionroot "${dataset}"; then + error "given dataset is not an encryptionroot: ${dataset}" + fi +} + +main() { + + if [ $# -eq 1 -a "${1:-}" == "--summary" ]; then + echo "$SUMMARY" + exit 0 + fi + + local dataset= + local pin= + local cfg= + local key= + local overwrite= + while getopts ":hfd:k:" o; do + case "$o" in + f) overwrite='yes' ;; + d) dataset="$OPTARG";; + k) key="$OPTARG";; + *) error "unrecognized argument: -${OPTARG}";; + esac + done + + if [ -z "${dataset:-""}" ]; then + error "did not specify a device!" + fi + + check_valid_dataset "${dataset}" + + if ! pin=${@:$((OPTIND++)):1} || [ -z "$PIN" ]; then + error "Did not specify a pin!" + elif ! findexe clevis-encrypt-"${PIN}" &>/dev/null; then + error "'$PIN' is not a valid pin!" + fi + + if ! cfg=${@:$((OPTIND++)):1} || [ -z "$CFG" ]; then + error "Did not specify a pin config!" + fi + + bind_zfs_dataset "${dataset}" "${pin}" "${cfg}" "${key}" "${overwrite}" +} + +main "${@}" diff --git a/src/zfs/clevis-zfs-common b/src/zfs/clevis-zfs-common new file mode 100755 index 00000000..3d9b80be --- /dev/null +++ b/src/zfs/clevis-zfs-common @@ -0,0 +1,140 @@ +#!/bin/bash + +# zfs user properties are limited to 8192 bytes +zfs_userprop_max_size=8000 +zfs_userprop_max_size=800 # set smaller to test splitting/combining chunks + +zfs_userprop_prefix='latchset.clevis' +zfs_data_prop="${zfs_userprop_prefix}:data" +zfs_status_prop="${zfs_userprop_prefix}:status" + +function load_key() { + local dataset="${1}" + local key="${2?need keyinput argument}" + + # Get the existing passphrase/keyfile. + local existing_key + local keyfile + + case "${key}" in + "") IFS= read -r -s -p "Enter existing ZFS password for ${dataset}: " existing_key; + echo >&2 + ;; + -) IFS= read -r -s -p "" existing_key ;; + *) keyfile="${key}" + if [ -r "${keyfile}" ]; then + existing_key="$(< "${keyfile}")" + else + error "cannot read key file '${keyfile}'" + fi + ;; + esac + echo "${existing_key}" +} + +function error() { + usage + echo >&2 -e "ERROR: ${*}" + exit 1 +} + +findexe() { + while read -r -d: path; do + [ -f "${path}/${1}" ] && [ -x "${path}/${1}" ] && \ + echo "${path}/${1}" && return 0 + done <<< "${PATH}:" + return 1 +} + +zfs_get_prop() { + local dataset="${1}" + local prop="${2}" + shift 2 + zfs get -H -o value -slocal "${prop}" "${dataset}" "${@}" +} + +function cut_into_chunks() { + fold -w "${zfs_userprop_max_size}" +} + +function zfs_is_bound() { + local dataset="${1}" + [[ "$(zfs_get_prop "${dataset}" "${zfs_status_prop}" )" == 'bound' ]] +} + +function zfs_is_encryptionroot() { + local dataset="${1}" + zfs_get_prop "${dataset}" 'encryptionroot' + [[ "$(zfs_get_prop "${dataset}" 'encryptionroot' -snone )" == "${dataset}" ]] +} + +function zfs_test_key() { + local dataset="${1}" + zfs load-key -n -L prompt "${dataset}" &>/dev/null +} + +function zfs_load_key() { + local dataset="${1}" + zfs load-key -L prompt "${dataset}" &>/dev/null +} + +function zero_pad() { + local num="${1}" + local width="${2}" + printf "%0${width}d" "${num}" +} + + +function zfs_bind_clevis_data() { + local dataset="${1}" + local clevis_data="${2}" + + echo >&2 -n 'binding new clevis data... ' + clevis_chunks=( $(cut_into_chunks <<<"${clevis_data}") ) + last_index="$(( "${#clevis_chunks[@]}" - 1 ))" + width="${#last_index}" + + local chunk chunk_num + for i in $(seq 0 "${last_index}" ); do + chunk="${clevis_chunks[${i}]}" + # we add zero-padding so the props sort nicely when we want to combine + # them when we unlock + chunk_num="$(zero_pad "${i}" "${width}" )" + + # e.g. latchset.clevis:pin-01=chunk_data + zfs set "${zfs_data_prop}-${chunk_num}=${chunk}" "${dataset}" + done + echo >&2 'ok' + + # check if unlocking works + echo >&2 -n 'testing new clevis data... ' + if ! (zfs_get_clevis_data "${dataset}" | clevis decrypt | zfs_test_key "${dataset}"); then + zfs_wipe_clevis_data "${dataset}" + error "could not unlock dataset with clevis configuration: ${dataset}" + fi + echo >&2 'ok' + + zfs set "${zfs_status_prop}=bound" "${dataset}" +} + +function zfs_get_data_props() { + local dataset="${1}" + + #property HAS to be set, otherwise the grep doesn't work + local outputs="${2:-property}" + + zfs_get_prop "${dataset}" 'all' -o "${outputs}" | grep -F "${zfs_data_prop}" | sort +} + +function zfs_wipe_clevis_data() { + local dataset="${1}" + + for prop in $(zfs_get_prop "${dataset}" 'all' -o property | grep -F "${zfs_userprop_prefix}" ); do + zfs inherit "${prop}" "${dataset}" + done +} + +function zfs_get_clevis_data() { + local dataset="${1:?}" + zfs_get_data_props "${dataset}" 'property,value' | sort | awk '{print $2}' | tr -d '\n' +} diff --git a/src/zfs/clevis-zfs-list b/src/zfs/clevis-zfs-list new file mode 100755 index 00000000..757b76d9 --- /dev/null +++ b/src/zfs/clevis-zfs-list @@ -0,0 +1,33 @@ +#!/bin/bash +set -euo pipefail + +. clevis-zfs-common + +SUMMARY="List zfs datasets that are bound with clevis" + + + +function usage() { + cat >&2 <<-USAGE_END + Usage: clevis zfs list [pool] + + $SUMMARY: + + -f Do not prompt when overwriting configuration + + -d DEV The zfs dataset on which to perform binding + + -k KEY Non-interactively read zfs password from KEY file + -k - Non-interactively read zfs password from standard input + + USAGE_END +} + + +main() { + local poolname="${1:-}" + echo "The following ZFS datasets have been bound with clevis:" + zfs list -o "name,${zfs_status_prop}" -r ${poolname} | awk '{ if ($2=="bound") {print $1}}' | sort | sed 's/^/ /' +} + +main "${@}" diff --git a/src/zfs/clevis-zfs-unbind b/src/zfs/clevis-zfs-unbind new file mode 100755 index 00000000..41667c4f --- /dev/null +++ b/src/zfs/clevis-zfs-unbind @@ -0,0 +1,60 @@ +#!/bin/bash +set -euo pipefail + +. clevis-zfs-common + +SUMMARY="Unbinds a ZFS dataset (remove clevis)" + + + +function usage() { + cat >&2 <<-USAGE_END + Usage: clevis zfs unbind [-k KEY] -d DEV + + $SUMMARY: + + -d DEV The zfs dataset on which to perform binding + + -k KEY Non-interactively read zfs password from KEY file + -k - Non-interactively read zfs password from standard input + + USAGE_END +} + + +function main() { + if [ $# -eq 1 -a "${1:-}" == "--summary" ]; then + echo "$SUMMARY" + exit 0 + fi + + local dataset= + local key= + while getopts ":hfd:k:" o; do + case "$o" in + d) dataset="$OPTARG";; + k) key="$OPTARG";; + *) error "unrecognized argument: -${OPTARG}";; + esac + done + + if [ -z "${dataset:-""}" ]; then + error "did not specify a device!" + fi + + if ! zfs_is_bound "${dataset}"; then + error "dataset is not bound with clevis: ${dataset}" + fi + + echo >&2 "Loading existing key... " + local existing_key="$(load_key "${dataset}" "${key}")" + + if ! zfs_test_key "${dataset}" <<<"${existing_key}"; then + error "given key does not unlock ${dataset}" + fi + + echo >&2 -n 'wiping clevis data... ' + zfs_wipe_clevis_data "${dataset}" + echo >&2 'ok' +} +main "${@}" diff --git a/src/zfs/clevis-zfs-unlock b/src/zfs/clevis-zfs-unlock new file mode 100755 index 00000000..9a44a44c --- /dev/null +++ b/src/zfs/clevis-zfs-unlock @@ -0,0 +1,66 @@ +#!/bin/bash +set -euo pipefail + +. clevis-zfs-common + +SUMMARY="Unlock a ZFS dataset using the saved clevis data" + +function usage() { + cat >&2 <<-USAGE_END + Usage: clevis zfs unlock [-n] [-k KEY] -d DATASET PIN CFG + + $SUMMARY: + + -t Test the clevis configuration without unlocking + + -d DATASET The zfs dataset to unlock + + USAGE_END +} + +function main() { + if [ $# -eq 1 -a "${1:-}" == "--summary" ]; then + echo "$SUMMARY" + exit 0 + fi + + local dataset + local test_only='false' + while getopts ":d:t" o; do + case "$o" in + d) dataset="$OPTARG" ;; + t) test_only='true' ;; + *) error "unrecognized argument: -${OPTARG}" ;; + esac + done + + if [ -z "${dataset:-""}" ]; then + error "did not specify a device!" + fi + + if ! zfs_is_bound "${dataset}"; then + error "dataset is not bound with clevis: ${dataset}" + fi + + local clevis_data password + + echo >&2 -n "loading clevis data from ${dataset}... " + clevis_data="$(zfs_get_clevis_data "${dataset}")" + password="$(clevis decrypt <<<"${clevis_data}")" + echo >&2 'ok' + + if [[ "${test_only}" == 'true' ]]; then + echo >&2 -n "testing key for ${dataset}... " + if ! zfs_test_key "${dataset}" <<<"${password}"; then + error "testing key for ${dataset} failed" + fi + else + echo >&2 -n "unlocking ${dataset}... " + if ! zfs_load_key "${dataset}" <<<"${clevis_data}"; then + error "could not load key for ${dataset}" + fi + fi + echo >&2 'ok' +} + +main "${@}" From 4b1912a33d72f64e2f6bfee079426cf31b793f7d Mon Sep 17 00:00:00 2001 From: Vince van Oosten Date: Tue, 24 May 2022 22:02:46 +0200 Subject: [PATCH 02/20] bugfixes --- src/zfs/clevis-zfs-bind | 9 +++++---- src/zfs/clevis-zfs-unlock | 2 +- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/zfs/clevis-zfs-bind b/src/zfs/clevis-zfs-bind index df957956..f18b76c9 100755 --- a/src/zfs/clevis-zfs-bind +++ b/src/zfs/clevis-zfs-bind @@ -91,17 +91,18 @@ main() { check_valid_dataset "${dataset}" - if ! pin=${@:$((OPTIND++)):1} || [ -z "$PIN" ]; then + if ! pin=${@:$((OPTIND++)):1} || [ -z "$pin" ]; then error "Did not specify a pin!" - elif ! findexe clevis-encrypt-"${PIN}" &>/dev/null; then - error "'$PIN' is not a valid pin!" + elif ! findexe clevis-encrypt-"${pin}" &>/dev/null; then + error "'$pin' is not a valid pin!" fi - if ! cfg=${@:$((OPTIND++)):1} || [ -z "$CFG" ]; then + if ! cfg=${@:$((OPTIND++)):1} || [ -z "$cfg" ]; then error "Did not specify a pin config!" fi bind_zfs_dataset "${dataset}" "${pin}" "${cfg}" "${key}" "${overwrite}" + echo >&2 "dataset ${dataset} is succesfully bound" } main "${@}" diff --git a/src/zfs/clevis-zfs-unlock b/src/zfs/clevis-zfs-unlock index 9a44a44c..f1506cbe 100755 --- a/src/zfs/clevis-zfs-unlock +++ b/src/zfs/clevis-zfs-unlock @@ -56,7 +56,7 @@ function main() { fi else echo >&2 -n "unlocking ${dataset}... " - if ! zfs_load_key "${dataset}" <<<"${clevis_data}"; then + if ! zfs_load_key "${dataset}" <<<"${password}"; then error "could not load key for ${dataset}" fi fi From d5d32e88be9490c1d70ba97b51e83f741e81095c Mon Sep 17 00:00:00 2001 From: Vince van Oosten Date: Wed, 25 May 2022 21:48:09 +0200 Subject: [PATCH 03/20] add support for labeled slots --- src/zfs/clevis-zfs-bind | 55 +++++++------ src/zfs/clevis-zfs-common | 167 +++++++++++++++++++++++++++++++------- src/zfs/clevis-zfs-unbind | 28 ++++--- src/zfs/clevis-zfs-unlock | 7 +- 4 files changed, 188 insertions(+), 69 deletions(-) diff --git a/src/zfs/clevis-zfs-bind b/src/zfs/clevis-zfs-bind index f18b76c9..049379d6 100755 --- a/src/zfs/clevis-zfs-bind +++ b/src/zfs/clevis-zfs-bind @@ -1,12 +1,9 @@ #!/bin/bash set -euo pipefail -. clevis-zfs-common SUMMARY="Binds a ZFS dataset using the specified policy" - - function usage() { cat >&2 <<-USAGE_END Usage: clevis zfs bind [-f] [-k KEY] -d DATASET PIN CFG @@ -16,6 +13,7 @@ function usage() { -f Do not prompt when overwriting configuration -d DATASET The zfs dataset on which to perform binding + -l LABEL The label to use for this binding, can use letters, numbers and underscores -k KEY Non-interactively read zfs password from KEY file -k - Non-interactively read zfs password from standard input @@ -26,18 +24,19 @@ function usage() { function bind_zfs_dataset() { local dataset="${1}" - local pin="${2}" - local cfg="${3}" - local key="${4}" - local overwrite="${5:-}" + local label="${2}" + local pin="${3}" + local cfg="${4}" + local key="${5}" + local overwrite="${6:-}" local existing_key clevis_data - if [[ -z "${overwrite}" ]] && zfs_is_bound "${dataset}"; then - error "given dataset already has a clevis binding, not overwriting: ${dataset}." + if [[ -z "${overwrite}" ]] && zfs_is_bound "${dataset}" "${label}"; then + error "given label ${label} in dataset ${dataset} already has a clevis binding, not overwritin." fi - existing_key="$(load_key "${dataset}" "${key}")" + existing_key="$(read_key "${dataset}" "${key}")" if ! zfs_test_key "${dataset}" <<<"${existing_key}"; then error "given key does not unlock ${dataset}" @@ -47,9 +46,9 @@ function bind_zfs_dataset() { clevis_data="$(clevis encrypt "${pin}" "${cfg}" <<<"${existing_key}" )" echo >&2 'ok' - [[ -n "${overwrite}" ]] && zfs_wipe_clevis_data "${dataset}" && echo >&2 'wiped old clevis data' + [[ -n "${overwrite}" ]] && zfs_wipe_clevis_label "${dataset}" "${label}" && echo >&2 'wiped old clevis data' - zfs_bind_clevis_data "${dataset}" "${clevis_data}" + zfs_bind_clevis_data "${dataset}" "${label}" "${clevis_data}" } function check_valid_dataset() { @@ -64,22 +63,24 @@ function check_valid_dataset() { fi } -main() { +function main() { - if [ $# -eq 1 -a "${1:-}" == "--summary" ]; then + if [ $# -eq 1 ] && [ "${1:-}" == "--summary" ]; then echo "$SUMMARY" exit 0 fi - local dataset= - local pin= - local cfg= - local key= - local overwrite= - while getopts ":hfd:k:" o; do + local dataset + local pin + local cfg + local label + local key + local overwrite='' + while getopts ":hfd:l:k:" o; do case "$o" in f) overwrite='yes' ;; d) dataset="$OPTARG";; + l) label="$OPTARG";; k) key="$OPTARG";; *) error "unrecognized argument: -${OPTARG}";; esac @@ -91,6 +92,11 @@ main() { check_valid_dataset "${dataset}" + if [ -z "${label:-}" ]; then + error "Did not specify a label!" + fi + + if ! pin=${@:$((OPTIND++)):1} || [ -z "$pin" ]; then error "Did not specify a pin!" elif ! findexe clevis-encrypt-"${pin}" &>/dev/null; then @@ -101,8 +107,11 @@ main() { error "Did not specify a pin config!" fi - bind_zfs_dataset "${dataset}" "${pin}" "${cfg}" "${key}" "${overwrite}" - echo >&2 "dataset ${dataset} is succesfully bound" + bind_zfs_dataset "${dataset}" "${label}" "${pin}" "${cfg}" "${key}" "${overwrite}" + echo >&2 "label ${label} on dataset ${dataset} is succesfully bound" } -main "${@}" +if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then + . clevis-zfs-common + main "${@}" +fi diff --git a/src/zfs/clevis-zfs-common b/src/zfs/clevis-zfs-common index 3d9b80be..1e5c0810 100755 --- a/src/zfs/clevis-zfs-common +++ b/src/zfs/clevis-zfs-common @@ -1,14 +1,82 @@ #!/bin/bash +set -euo pipefail # zfs user properties are limited to 8192 bytes zfs_userprop_max_size=8000 zfs_userprop_max_size=800 # set smaller to test splitting/combining chunks +# all clevis userprops will be prefixed with "latchset.clevis:" as suggested by +# the User Properties section in zfsprops(8) zfs_userprop_prefix='latchset.clevis' -zfs_data_prop="${zfs_userprop_prefix}:data" -zfs_status_prop="${zfs_userprop_prefix}:status" -function load_key() { +# This contains the space-separated list of labels that have been bound with clevis +zfs_labels_prop="${zfs_userprop_prefix}:labels" + +# The data for each label is saved into one or more zfs properties. +# E.g. the label 'mybinding' with data of 20k bytes and label 'other' with 4k bytes +# we suffix the label in the :labels property so we can easily find all numbered parts +# - latchset.clevis:labels = "mybinding:2 other" +# - latchset.clevis.label:mybinding-0 = "[clevis data first 8k]" +# - latchset.clevis.label:mybinding-1 = "[clevis data second 8k]" +# - latchset.clevis.label:mybinding-2 = "[clevis data final 4k]" +# - latchset.clevis.label:other = [clevis data 4k] +zfs_label_prefix="${zfs_userprop_prefix}.label" + + +function zfs_get_labels() { + local dataset="${1}" + zfs_get_prop "${dataset}" "${zfs_labels_prop}" +} + +function zfs_set_labels() { + local dataset="${1}"; + local new_labels="${2}" + echo >&2 "setting new labels: ${new_labels}" + zfs set "${zfs_labels_prop}=${new_labels}" "${dataset}" +} + +function zfs_add_label() { + local dataset="${1}" + local new_label="${2}" + local labels=( $(zfs_get_labels "${dataset}") ) + local labels+=( "${new_label}" ) + zfs_set_labels "${dataset}" "${labels[*]}" +} + +function zfs_remove_label() { + local dataset="${1}" + local old_label="${2}" + local labels=( $(zfs_get_labels "${dataset}") ) + local new_labels=( "${labels[@]/${old_label}}" ) + zfs_set_labels "${dataset}" "${new_labels[*]}" +} + +# valid characters of userprops are: [0-9a-z:._-] +# valid characters of clevis-zfs labels are: [0-9a-z_] +function is_valid_label() { + local label="${1}" + # The length limit is quite arbitrary; but we have to draw the line + # somewhere and zfs-user-property names can be at most 256 characters long. + # We can't use all 256 characters because we need some space in the + # property name for the zfs_label_prefix and the chunk_counter suffix. + local limit=100 + local regex='^[0-9a-z_]+$' + + if [[ "${#label}" -gt "${limit}" ]]; then + echo >&2 "label is longer than ${limit}: ${label}" + return 1 + fi + + if [[ "${label}" =~ ${regex} ]]; then + return 0 + else + echo >&2 "label is invalid: '${label}'. Expecting an alphanumeric string " + return 1 + fi +} + + +function read_key() { local dataset="${1}" local key="${2?need keyinput argument}" @@ -50,7 +118,7 @@ zfs_get_prop() { local dataset="${1}" local prop="${2}" shift 2 - zfs get -H -o value -slocal "${prop}" "${dataset}" "${@}" + zfs get "${prop}" "${dataset}" -H -o value -slocal "${@}" } function cut_into_chunks() { @@ -59,12 +127,21 @@ function cut_into_chunks() { function zfs_is_bound() { local dataset="${1}" - [[ "$(zfs_get_prop "${dataset}" "${zfs_status_prop}" )" == 'bound' ]] + local label_to_check="${2:-}" + local label="${label_to_check%:*}" + + local labels="$(zfs_get_labels "${dataset}")" + if [[ -n "${label}" ]]; then + [[ " ${labels} " == *" ${label} "* ]] && return 0 + else + # we don't check for a specific label, just that at least one label is set + [[ "${#current_labels[@]}" -gt 0 ]] && return 0 + fi + return 1 } function zfs_is_encryptionroot() { local dataset="${1}" - zfs_get_prop "${dataset}" 'encryptionroot' [[ "$(zfs_get_prop "${dataset}" 'encryptionroot' -snone )" == "${dataset}" ]] } @@ -87,34 +164,43 @@ function zero_pad() { function zfs_bind_clevis_data() { local dataset="${1}" - local clevis_data="${2}" + local label="${2}" + local clevis_data="${3}" + + local zfs_label_prop="${zfs_label_prefix}:${label}" echo >&2 -n 'binding new clevis data... ' - clevis_chunks=( $(cut_into_chunks <<<"${clevis_data}") ) - last_index="$(( "${#clevis_chunks[@]}" - 1 ))" - width="${#last_index}" - - local chunk chunk_num - for i in $(seq 0 "${last_index}" ); do - chunk="${clevis_chunks[${i}]}" - # we add zero-padding so the props sort nicely when we want to combine - # them when we unlock - chunk_num="$(zero_pad "${i}" "${width}" )" - - # e.g. latchset.clevis:pin-01=chunk_data - zfs set "${zfs_data_prop}-${chunk_num}=${chunk}" "${dataset}" - done + # use a single prop without number suffix if it will fit in one prop + if [[ "${#clevis_data}" -lt "${zfs_userprop_max_size}" ]]; then + zfs set "${zfs_label_prop}=${clevis_data}" "${dataset}" + else + clevis_chunks=( $(cut_into_chunks <<<"${clevis_data}") ) + last_index="$(( "${#clevis_chunks[@]}" - 1 ))" + width="${#last_index}" + + local chunk chunk_num + for i in $(seq 0 "${last_index}" ); do + chunk="${clevis_chunks[${i}]}" + # we add zero-padding so the props sort nicely when we want to combine + # them when we unlock + chunk_num="$(zero_pad "${i}" "${width}" )" + + # e.g. latchset.clevis.label:${label}-01=chunk_data + zfs set "${zfs_label_prop}-${chunk_num}=${chunk}" "${dataset}" + done + + label="${label}:${last_index}" + fi echo >&2 'ok' # check if unlocking works echo >&2 -n 'testing new clevis data... ' - if ! (zfs_get_clevis_data "${dataset}" | clevis decrypt | zfs_test_key "${dataset}"); then - zfs_wipe_clevis_data "${dataset}" + if ! (zfs_get_clevis_label "${dataset}" "${label}" | clevis decrypt | zfs_test_key "${dataset}"); then + zfs_wipe_clevis_label "${dataset}" "${label}" error "could not unlock dataset with clevis configuration: ${dataset}" fi echo >&2 'ok' - - zfs set "${zfs_status_prop}=bound" "${dataset}" + zfs_add_label "${dataset}" "${label}" } function zfs_get_data_props() { @@ -123,18 +209,37 @@ function zfs_get_data_props() { #property HAS to be set, otherwise the grep doesn't work local outputs="${2:-property}" - zfs_get_prop "${dataset}" 'all' -o "${outputs}" | grep -F "${zfs_data_prop}" | sort + zfs_get_prop "${dataset}" 'all' -o "${outputs}" | grep -F "${zfs_label_prefix}" | sort } -function zfs_wipe_clevis_data() { +function zfs_wipe_clevis_label() { local dataset="${1}" + local label="${2}" - for prop in $(zfs_get_prop "${dataset}" 'all' -o property | grep -F "${zfs_userprop_prefix}" ); do + local zfs_label_prop="${zfs_label_prefix}:${label}" + + for prop in $(zfs_get_prop "${dataset}" 'all' -o property | grep -F "${zfs_label_prop}" ); do zfs inherit "${prop}" "${dataset}" done } -function zfs_get_clevis_data() { - local dataset="${1:?}" - zfs_get_data_props "${dataset}" 'property,value' | sort | awk '{print $2}' | tr -d '\n' +function zfs_get_clevis_label() { + local dataset="${1}" + local label="${2%:*}" + local last_index="${2#*:}" + + if [[ -z "${last_index}" ]]; then + zfs_label_prop="${zfs_label_prefix}:${label}" + zfs_get_prop "${dataset}" "${zfs_label_prop}" + else + for num in $(seq -w 0 "${last_index}"); do + zfs_get_prop "${dataset}" "${zfs_label_prop}-${num}" | tr -d '\n' + done + fi } + + +if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then + . clevis-zfs-test + _test "$@" +fi diff --git a/src/zfs/clevis-zfs-unbind b/src/zfs/clevis-zfs-unbind index 41667c4f..3b021aa4 100755 --- a/src/zfs/clevis-zfs-unbind +++ b/src/zfs/clevis-zfs-unbind @@ -1,39 +1,42 @@ #!/bin/bash -set -euo pipefail +set -eu . clevis-zfs-common -SUMMARY="Unbinds a ZFS dataset (remove clevis)" +SUMMARY="Unbinds a label from a ZFS dataset" function usage() { cat >&2 <<-USAGE_END - Usage: clevis zfs unbind [-k KEY] -d DEV + Usage: clevis zfs unbind [-k KEY] -d DATASET -l LABEL $SUMMARY: - -d DEV The zfs dataset on which to perform binding + -d DATASET The zfs dataset on which to perform unbinding + -l LABEL The label to unbind - -k KEY Non-interactively read zfs password from KEY file - -k - Non-interactively read zfs password from standard input + -k KEY Non-interactively read zfs password from KEY file + -k - Non-interactively read zfs password from standard input USAGE_END } function main() { - if [ $# -eq 1 -a "${1:-}" == "--summary" ]; then + if [ $# -eq 1 ] && [ "${1:-}" == "--summary" ]; then echo "$SUMMARY" exit 0 fi - local dataset= - local key= - while getopts ":hfd:k:" o; do + local dataset + local key + local label + while getopts ":hfd:k:l:" o; do case "$o" in d) dataset="$OPTARG";; k) key="$OPTARG";; + l) label="$OPTARG";; *) error "unrecognized argument: -${OPTARG}";; esac done @@ -46,15 +49,16 @@ function main() { error "dataset is not bound with clevis: ${dataset}" fi + local existing_key echo >&2 "Loading existing key... " - local existing_key="$(load_key "${dataset}" "${key}")" + existing_key="$(read_key "${dataset}" "${key}")" if ! zfs_test_key "${dataset}" <<<"${existing_key}"; then error "given key does not unlock ${dataset}" fi echo >&2 -n 'wiping clevis data... ' - zfs_wipe_clevis_data "${dataset}" + zfs_wipe_clevis_data "${dataset}" "${label}" echo >&2 'ok' } main "${@}" diff --git a/src/zfs/clevis-zfs-unlock b/src/zfs/clevis-zfs-unlock index f1506cbe..9f1bb9b6 100755 --- a/src/zfs/clevis-zfs-unlock +++ b/src/zfs/clevis-zfs-unlock @@ -1,8 +1,6 @@ #!/bin/bash set -euo pipefail -. clevis-zfs-common - SUMMARY="Unlock a ZFS dataset using the saved clevis data" function usage() { @@ -63,4 +61,7 @@ function main() { echo >&2 'ok' } -main "${@}" +if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then + . clevis-zfs-common + main "${@}" +fi From 21eab7c92b6bfac836658866b210fc657410a5b0 Mon Sep 17 00:00:00 2001 From: Vince van Oosten Date: Wed, 25 May 2022 23:23:55 +0200 Subject: [PATCH 04/20] add more tests for zfs functions (not complete) --- src/zfs/clevis-zfs-common | 36 ++++--- src/zfs/clevis-zfs-list | 31 +++--- src/zfs/clevis-zfs-test | 192 ++++++++++++++++++++++++++++++++++++++ src/zfs/clevis-zfs-unbind | 6 +- src/zfs/clevis-zfs-unlock | 39 ++++---- 5 files changed, 248 insertions(+), 56 deletions(-) create mode 100755 src/zfs/clevis-zfs-test diff --git a/src/zfs/clevis-zfs-common b/src/zfs/clevis-zfs-common index 1e5c0810..8721d02b 100755 --- a/src/zfs/clevis-zfs-common +++ b/src/zfs/clevis-zfs-common @@ -31,7 +31,6 @@ function zfs_get_labels() { function zfs_set_labels() { local dataset="${1}"; local new_labels="${2}" - echo >&2 "setting new labels: ${new_labels}" zfs set "${zfs_labels_prop}=${new_labels}" "${dataset}" } @@ -135,7 +134,7 @@ function zfs_is_bound() { [[ " ${labels} " == *" ${label} "* ]] && return 0 else # we don't check for a specific label, just that at least one label is set - [[ "${#current_labels[@]}" -gt 0 ]] && return 0 + [[ "${#labels}" -gt 0 ]] && return 0 fi return 1 } @@ -146,13 +145,13 @@ function zfs_is_encryptionroot() { } function zfs_test_key() { - local dataset="${1}" - zfs load-key -n -L prompt "${dataset}" &>/dev/null + zfs_load_key "${1}" 'dry_run' } function zfs_load_key() { local dataset="${1}" - zfs load-key -L prompt "${dataset}" &>/dev/null + local dry_run="${2:+-n}" + zfs load-key ${dry_run} -L prompt "${dataset}" >/dev/null } function zero_pad() { @@ -203,24 +202,21 @@ function zfs_bind_clevis_data() { zfs_add_label "${dataset}" "${label}" } -function zfs_get_data_props() { - local dataset="${1}" - - #property HAS to be set, otherwise the grep doesn't work - local outputs="${2:-property}" - - zfs_get_prop "${dataset}" 'all' -o "${outputs}" | grep -F "${zfs_label_prefix}" | sort -} - function zfs_wipe_clevis_label() { local dataset="${1}" - local label="${2}" + local label="${2%:*}" + local last_index="${2#*:}" local zfs_label_prop="${zfs_label_prefix}:${label}" - for prop in $(zfs_get_prop "${dataset}" 'all' -o property | grep -F "${zfs_label_prop}" ); do - zfs inherit "${prop}" "${dataset}" - done + if [[ "${label}" == "${last_index}" ]]; then + zfs inherit "${zfs_label_prop}" "${dataset}" + else + for num in $(seq -w 0 "${last_index}"); do + zfs inherit "${zfs_label_prop}-${num}" "${dataset}" + done + fi + zfs_remove_label "${dataset}" "${label}" } function zfs_get_clevis_label() { @@ -228,8 +224,8 @@ function zfs_get_clevis_label() { local label="${2%:*}" local last_index="${2#*:}" - if [[ -z "${last_index}" ]]; then - zfs_label_prop="${zfs_label_prefix}:${label}" + local zfs_label_prop="${zfs_label_prefix}:${label}" + if [[ "${label}" == "${last_index}" ]]; then zfs_get_prop "${dataset}" "${zfs_label_prop}" else for num in $(seq -w 0 "${last_index}"); do diff --git a/src/zfs/clevis-zfs-list b/src/zfs/clevis-zfs-list index 757b76d9..f6e095ae 100755 --- a/src/zfs/clevis-zfs-list +++ b/src/zfs/clevis-zfs-list @@ -3,31 +3,34 @@ set -euo pipefail . clevis-zfs-common -SUMMARY="List zfs datasets that are bound with clevis" - - +SUMMARY="List zfs datasets that are bound with clevis [in dataset]" function usage() { cat >&2 <<-USAGE_END - Usage: clevis zfs list [pool] + Usage: clevis zfs list -d DATASET $SUMMARY: - -f Do not prompt when overwriting configuration - - -d DEV The zfs dataset on which to perform binding - - -k KEY Non-interactively read zfs password from KEY file - -k - Non-interactively read zfs password from standard input - USAGE_END } main() { - local poolname="${1:-}" - echo "The following ZFS datasets have been bound with clevis:" - zfs list -o "name,${zfs_status_prop}" -r ${poolname} | awk '{ if ($2=="bound") {print $1}}' | sort | sed 's/^/ /' + if [ $# -eq 1 -a "${1:-}" == "--summary" ]; then + echo "$SUMMARY" + exit 0 + fi + + local dataset + while getopts "d:" o; do + case "$o" in + d) dataset="$OPTARG" ;; + *) error "unrecognized argument: -${OPTARG}" ;; + esac + done + + echo >&2 "The following ZFS datasets have been bound with clevis:" + zfs get -r -H -o name,value -slocal "${zfs_labels_prop}" ${dataset} } main "${@}" diff --git a/src/zfs/clevis-zfs-test b/src/zfs/clevis-zfs-test new file mode 100755 index 00000000..55937514 --- /dev/null +++ b/src/zfs/clevis-zfs-test @@ -0,0 +1,192 @@ +#!/bin/bash +set -euo pipefail + +zpool='clevis-zfs-pool' +root_dataset="${zpool}/clevis" +test_dataset="${root_dataset}/test" +tang_host='TANG_HOST' +tang_thp='TANG_THP' + +tang_host='192.168.178.26:8565' +tang_thp='NHt3FdBvUX0AiHZycp2emEzfYIc' + +function zfs_usage() { + local permissions='create,destroy,mount,load-key,change-key,userprop,encryption,keyformat,keylocation' + cat >&2 <<-EOF + + To test the zfs functions you will need to do the following as root: + + 1) make sure the zfs kernel module is loaded: + modprobe zfs + + 2) create a new backing file: + dd if=/dev/zero bs=10M count=20 conv=fdatasync of=/tmp/clevis-zfs-pool + + 3) create an unencrypted zfs pool using the backing file: + zpool create ${zpool} /tmp/clevis-zfs-pool + + 4) create an unencrypted child dataset so we can grant access to a non-root user: + zpool create ${root_dataset} + + 4) give the user running the test script permissions to make changes to this pool: + zfs allow ${USER} ${permissions} ${root_dataset} + EOF +} + +exit_code=0 +testing_password="paaaassssswoooooorrrrddd" + +function testing() { + echo >&2 -en "${FUNCNAME[1]}: ${*}... " +} +function success() { + echo >&2 'ok' +} +function failed() { + echo >&2 "failed!" + echo >&2 "${BASH_SOURCE[0]}:${BASH_LINENO[0]} EXPECTED: '${expected}' GOT: '${result}' ${*}" + declare -g exit_code=1 +} + +function _test() { + _test_is_valid_label + _test_read_key + _test_zero_pad + _test_zfs_functions + [[ "${exit_code}" -gt 0 ]] && echo >&2 "SOME TESTS HAVE FAILED" + exit "${exit_code}" +} + + + +function _test_is_valid_label() { + local expected + local result + valid_labels=( + 2 # single digit + 0 # single 0 + a # single letter + 3a # double + a4 # double + 1024 # all digits + abcd # all letters + 0a0ab # alphanum + a_bc # with underscore + ) + + invalid_labels=( + a-bc # with dash + a.bc # with dot + a:bc # with colon + a.1_c-2:e # with all + '' # empty string + '.' # just a dot + '-' # just a dash + AteA_ # capitals + aa@a # @-symbol + aa/a # /-symbol + aa/a # /-symbol + aa?a # ?-symbol + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa # 101-chars long + ) + + testing 'testing valid labels' + expected=0 + result=1 + for l in "${valid_labels[@]}"; do + if ! is_valid_label "${l}" &>/dev/null; then + failed "for is_valid_label '${l}'" + fi + done + success + + testing 'testing invalid labels' + expected=1 + result=0 + for l in "${invalid_labels[@]}"; do + if is_valid_label "${l}" &>/dev/null; then + failed "for \`is_valid_label '${l}'\`" + fi + done + success +} + + +function _test_read_key() { + # test with reading from stdin + local expected + local result + + testing 'key test: no key argument (stdin)' + expected='no-argument-password' + result="$(read_key "mydataset" '' <<<"${expected}" 2>/dev/null)" + [[ "${expected}" == "${result}" ]] && success || failed + + testing 'key test: dash argument (stdin)' + expected='dash-argument-password' + result="$(read_key "mydataset" '-' <<<"${expected}" 2>/dev/null)" + [[ "${expected}" == "${result}" ]] && success || failed + + + testing 'key arg: filename' + expected='filename-argument-password' + result="$(read_key "mydataset" <(echo "${expected}") 2>/dev/null)" + [[ "${expected}" == "${result}" ]] && success || failed +} + + +function _test_zero_pad() { + testing 'pad with 4 zeroes' + local expected='000051' + local result="$(zero_pad 51 6)" + [[ "${expected}" == "${result}" ]] && success || failed +} + + +function _zfs_test_teardown() { + testing "removing zfs testing dataset: ${test_dataset}" + ! zfs list "${test_dataset}" &>/dev/null && success && return 0 + zfs destroy -f -r "${test_dataset}" + success +} + +function _zfs_create_encrypted_dataset() { + local dataset="${1}" + testing "creating encrypted test dataset: ${1}" + # zfs create will work with the permissions as described, but will exit + # with an error code because mounting failed + zfs create "${1}" -o encryption=on -o keyformat=passphrase <<<"${testing_password}" &>/dev/null || true + # we need to make sure the dataset is created because of the error-code shenanigans + zfs list "${1}" &>/dev/null && success || return 1 +} + +function _test_zfs_functions() { + testing "checking if zfs testing dataset exists: ${root_dataset}" + zfs list "${root_dataset}" &>/dev/null && success || (zfs_usage; return 1) + trap _zfs_test_teardown EXIT + _zfs_create_encrypted_dataset "${test_dataset}" + + testing 'binding zfs dataset twice' + echo "${testing_password}" | ./clevis-zfs-bind -d "${test_dataset}" -k - -l 'testlabel' tang '{"url": "http://'${tang_host}'", "thp": "'${tang_thp}'"}' &>/dev/null + echo "${testing_password}" | ./clevis-zfs-bind -d "${test_dataset}" -k - -l 'another_testlabel' tang '{"url": "http://'${tang_host}'", "thp": "'${tang_thp}'"}' &>/dev/null + success + + testing 'listing labels' + expected="${test_dataset}"$'\ttestlabel:1 another_testlabel:1' + result="$(./clevis-zfs-list -d "${test_dataset}" 2>/dev/null)" + [[ "${expected}" == "${result}" ]] && success || failed + + testing 'testing unlocking with bindings' + ./clevis-zfs-unlock -t -d "${test_dataset}" && success + + testing 'unlocking with binding' + zfs unload-key "${test_dataset}" + [[ "$(zfs_get_prop "${test_dataset}" 'keystatus' -snone)" == 'unavailable' ]] || return 1 + ./clevis-zfs-unlock -d "${test_dataset}" + success + + testing 'unbinding dataset twice' + echo "${testing_password}" | ./clevis-zfs-unbind -d "${test_dataset}" -l 'another_testlabel' -k - + echo "${testing_password}" | ./clevis-zfs-unbind -d "${test_dataset}" -l 'testlabel' -k - + success +} diff --git a/src/zfs/clevis-zfs-unbind b/src/zfs/clevis-zfs-unbind index 3b021aa4..1ca0bc8c 100755 --- a/src/zfs/clevis-zfs-unbind +++ b/src/zfs/clevis-zfs-unbind @@ -1,12 +1,11 @@ #!/bin/bash -set -eu +set -euo pipefail . clevis-zfs-common SUMMARY="Unbinds a label from a ZFS dataset" - function usage() { cat >&2 <<-USAGE_END Usage: clevis zfs unbind [-k KEY] -d DATASET -l LABEL @@ -58,7 +57,8 @@ function main() { fi echo >&2 -n 'wiping clevis data... ' - zfs_wipe_clevis_data "${dataset}" "${label}" + zfs_wipe_clevis_label "${dataset}" "${label}" echo >&2 'ok' } + main "${@}" diff --git a/src/zfs/clevis-zfs-unlock b/src/zfs/clevis-zfs-unlock index 9f1bb9b6..b00be846 100755 --- a/src/zfs/clevis-zfs-unlock +++ b/src/zfs/clevis-zfs-unlock @@ -10,8 +10,8 @@ function usage() { $SUMMARY: -t Test the clevis configuration without unlocking - -d DATASET The zfs dataset to unlock + -l LABEL Use only this label to unlock (defaults to trying all labels): TODO USAGE_END } @@ -23,11 +23,13 @@ function main() { fi local dataset - local test_only='false' - while getopts ":d:t" o; do + local test_only='' + local label='' + while getopts ":d:l:t" o; do case "$o" in d) dataset="$OPTARG" ;; - t) test_only='true' ;; + t) test_only=' (test)' ;; + l) label="$OPTARG" ;; *) error "unrecognized argument: -${OPTARG}" ;; esac done @@ -42,23 +44,22 @@ function main() { local clevis_data password - echo >&2 -n "loading clevis data from ${dataset}... " - clevis_data="$(zfs_get_clevis_data "${dataset}")" - password="$(clevis decrypt <<<"${clevis_data}")" - echo >&2 'ok' + for label in $(zfs_get_labels "${dataset}" | tr ' ' '\n' | grep "^${label}(:|$)"); do + echo >&2 -n "loading clevis data from label ${label}... " + clevis_data="$(zfs_get_clevis_label "${dataset}" "${label}")" + password="$(clevis decrypt <<<"${clevis_data}" || echo '' )" + echo >&2 'ok' - if [[ "${test_only}" == 'true' ]]; then - echo >&2 -n "testing key for ${dataset}... " - if ! zfs_test_key "${dataset}" <<<"${password}"; then - error "testing key for ${dataset} failed" - fi - else - echo >&2 -n "unlocking ${dataset}... " - if ! zfs_load_key "${dataset}" <<<"${password}"; then - error "could not load key for ${dataset}" + echo >&2 -n "unlocking ${dataset}${test_only}... " + if echo "${password}" | zfs_load_key "${dataset}" "${test_only}"; then + echo >&2 'ok' + [[ -z "${test_only}" ]] && exit 0 + else + echo >&2 "failed" + continue fi - fi - echo >&2 'ok' + done + exit 1 } if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then From e3468ca9427b5f655d3d1b27f9105aeccf3c8c14 Mon Sep 17 00:00:00 2001 From: Vince van Oosten Date: Thu, 26 May 2022 10:22:51 +0200 Subject: [PATCH 05/20] update tang testing config --- src/zfs/clevis-zfs-test | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/zfs/clevis-zfs-test b/src/zfs/clevis-zfs-test index 55937514..d69855a6 100755 --- a/src/zfs/clevis-zfs-test +++ b/src/zfs/clevis-zfs-test @@ -4,11 +4,10 @@ set -euo pipefail zpool='clevis-zfs-pool' root_dataset="${zpool}/clevis" test_dataset="${root_dataset}/test" -tang_host='TANG_HOST' -tang_thp='TANG_THP' -tang_host='192.168.178.26:8565' -tang_thp='NHt3FdBvUX0AiHZycp2emEzfYIc' +tang_host='127.0.0.1' +tang_thp='TANG_THP' +tang_config='{"url": "http://'${tang_host}'", "thp": "'${tang_thp}'"}' function zfs_usage() { local permissions='create,destroy,mount,load-key,change-key,userprop,encryption,keyformat,keylocation' @@ -30,6 +29,8 @@ function zfs_usage() { 4) give the user running the test script permissions to make changes to this pool: zfs allow ${USER} ${permissions} ${root_dataset} + + 5) set tang_host and tang_thp to an existing tang server in ${BASH_SOURCE[0]} EOF } @@ -167,8 +168,8 @@ function _test_zfs_functions() { _zfs_create_encrypted_dataset "${test_dataset}" testing 'binding zfs dataset twice' - echo "${testing_password}" | ./clevis-zfs-bind -d "${test_dataset}" -k - -l 'testlabel' tang '{"url": "http://'${tang_host}'", "thp": "'${tang_thp}'"}' &>/dev/null - echo "${testing_password}" | ./clevis-zfs-bind -d "${test_dataset}" -k - -l 'another_testlabel' tang '{"url": "http://'${tang_host}'", "thp": "'${tang_thp}'"}' &>/dev/null + echo "${testing_password}" | ./clevis-zfs-bind -d "${test_dataset}" -k - -l 'testlabel' tang "${tang_config}" &>/dev/null + echo "${testing_password}" | ./clevis-zfs-bind -d "${test_dataset}" -k - -l 'another_testlabel' tang "${tang_config}" &>/dev/null success testing 'listing labels' From 7aba2889b3ecf8a744570d6abead478c1c304069 Mon Sep 17 00:00:00 2001 From: Vince van Oosten Date: Thu, 26 May 2022 14:40:42 +0200 Subject: [PATCH 06/20] reordered some code around --- src/zfs/clevis-zfs-bind | 28 ++-- src/zfs/clevis-zfs-common | 263 ++++++++++++++++++++++++++------------ src/zfs/clevis-zfs-test | 83 +++++++++--- src/zfs/clevis-zfs-unbind | 2 +- src/zfs/clevis-zfs-unlock | 52 +++++--- 5 files changed, 290 insertions(+), 138 deletions(-) diff --git a/src/zfs/clevis-zfs-bind b/src/zfs/clevis-zfs-bind index 049379d6..c274d5af 100755 --- a/src/zfs/clevis-zfs-bind +++ b/src/zfs/clevis-zfs-bind @@ -21,6 +21,14 @@ function usage() { USAGE_END } +findexe() { + while read -r -d: path; do + [ -f "${path}/${1}" ] && [ -x "${path}/${1}" ] && \ + echo "${path}/${1}" && return 0 + done <<< "${PATH}:" + return 1 +} + function bind_zfs_dataset() { local dataset="${1}" @@ -33,10 +41,10 @@ function bind_zfs_dataset() { local existing_key clevis_data if [[ -z "${overwrite}" ]] && zfs_is_bound "${dataset}" "${label}"; then - error "given label ${label} in dataset ${dataset} already has a clevis binding, not overwritin." + error "given label ${label} in dataset ${dataset} already has a clevis binding, not overwriting." fi - existing_key="$(read_key "${dataset}" "${key}")" + existing_key="$(read_passphrase "${dataset}" "${key}")" if ! zfs_test_key "${dataset}" <<<"${existing_key}"; then error "given key does not unlock ${dataset}" @@ -46,21 +54,9 @@ function bind_zfs_dataset() { clevis_data="$(clevis encrypt "${pin}" "${cfg}" <<<"${existing_key}" )" echo >&2 'ok' - [[ -n "${overwrite}" ]] && zfs_wipe_clevis_label "${dataset}" "${label}" && echo >&2 'wiped old clevis data' - - zfs_bind_clevis_data "${dataset}" "${label}" "${clevis_data}" -} - -function check_valid_dataset() { - local dataset="${1}" - - if ! zfs_get_prop "${dataset}" 'name' -snone &>/dev/null; then - error "${dataset} is not a zfs dataset!" - fi + [[ -n "${overwrite}" ]] && zfs_unbind_clevis_label "${dataset}" "${label}" && echo >&2 'unbound old clevis data' - if ! zfs_is_encryptionroot "${dataset}"; then - error "given dataset is not an encryptionroot: ${dataset}" - fi + zfs_bind_clevis_label "${dataset}" "${label}" "${clevis_data}" } function main() { diff --git a/src/zfs/clevis-zfs-common b/src/zfs/clevis-zfs-common index 8721d02b..28a15572 100755 --- a/src/zfs/clevis-zfs-common +++ b/src/zfs/clevis-zfs-common @@ -2,8 +2,8 @@ set -euo pipefail # zfs user properties are limited to 8192 bytes -zfs_userprop_max_size=8000 -zfs_userprop_max_size=800 # set smaller to test splitting/combining chunks +zfs_userprop_value_limit=8000 +zfs_userprop_name_limit=256 # all clevis userprops will be prefixed with "latchset.clevis:" as suggested by # the User Properties section in zfsprops(8) @@ -23,17 +23,97 @@ zfs_labels_prop="${zfs_userprop_prefix}:labels" zfs_label_prefix="${zfs_userprop_prefix}.label" +# Interfacing functions with ZFS +################################ +function zfs_remove_property() { + local dataset="${1}" + local property="${2}" + zfs inherit "${property}" "${dataset}" +} + +# valid characters of zfs user property names are: [0-9a-z:._-] (see zfsprops(7) ) +function zfs_set_property() { + local dataset="${1}" + local property="${2}" + local value="${3}" + [[ "${#property}" -le "${zfs_userprop_name_limit}" ]] || error "property name longer than ${zfs_userprop_name_limit} characters '${property}'" + [[ "${#value}" -le "${zfs_userprop_value_limit}" ]] || error "property value longer than ${zfs_userprop_value_limit} characters '${value}'" + zfs set "${property}=${value}" "${dataset}" +} + +# defaults to getting just the value of the given property and only when it is set directly on the dataset ("local") +zfs_get_property() { + local dataset="${1}" + local property="${2}" + shift 2 + zfs get "${property}" "${dataset}" -H -o value -slocal "${@}" +} + +function zfs_load_key() { + local dataset="${1}" + local dry_run="${2:+-n}" + zfs load-key ${dry_run} -L prompt "${dataset}" >/dev/null +} + +function zfs_test_key() { + zfs_load_key "${1}" 'dry_run' +} + +function zfs_unload_key() { + local dataset="${1}" + zfs unload-key "${dataset}" >/dev/null +} + +# ZFS properties functions to deal with clevis labels +############################################## + +# valid characters of clevis-zfs labels are: [0-9a-z_] +function is_valid_label() { + local label="${1}" + # This length limit is quite arbitrary; (as is the removal of [:.-] ) + # but we have to draw the line somewhere and zfs-user-property names + # can be at most 256 characters long. We can't use all 256 characters + # because we need some space in the property name for the + # zfs_label_prefix and the chunk_counter suffix. + local limit=100 + local regex='^[0-9a-z_]+$' + + if [[ "${#label}" -gt "${limit}" ]]; then + echo >&2 "label is longer than ${limit} characters: ${label}" + return 1 + fi + + if [[ "${label}" =~ ${regex} ]]; then + return 0 + else + echo >&2 "label is invalid: '${label}'. Valid characters: a-z, 0-9, _ (underscore)" + return 1 + fi +} + +# get a list of all labels, including possible number suffixes function zfs_get_labels() { local dataset="${1}" - zfs_get_prop "${dataset}" "${zfs_labels_prop}" + zfs_get_property "${dataset}" "${zfs_labels_prop}" +} + +# get a single label from the list of all labels, +# including possible number suffix +function zfs_get_label() { + local dataset="${1}" + local label="${2%%:*}" + zfs_get_labels "${dataset}" >&2 + zfs_get_labels "${dataset}" | tr ' ' '\n' | grep -E "^${label}(:|$)" } +# set the list of labels to the given value function zfs_set_labels() { local dataset="${1}"; local new_labels="${2}" - zfs set "${zfs_labels_prop}=${new_labels}" "${dataset}" + zfs_set_property "${dataset}" "${zfs_labels_prop}" "${new_labels}" } +# add a single label to the existing list of labels function zfs_add_label() { local dataset="${1}" local new_label="${2}" @@ -42,6 +122,7 @@ function zfs_add_label() { zfs_set_labels "${dataset}" "${labels[*]}" } +# remove a single label to the existing list of labels function zfs_remove_label() { local dataset="${1}" local old_label="${2}" @@ -50,32 +131,51 @@ function zfs_remove_label() { zfs_set_labels "${dataset}" "${new_labels[*]}" } -# valid characters of userprops are: [0-9a-z:._-] -# valid characters of clevis-zfs labels are: [0-9a-z_] -function is_valid_label() { - local label="${1}" - # The length limit is quite arbitrary; but we have to draw the line - # somewhere and zfs-user-property names can be at most 256 characters long. - # We can't use all 256 characters because we need some space in the - # property name for the zfs_label_prefix and the chunk_counter suffix. - local limit=100 - local regex='^[0-9a-z_]+$' +# functions for checking zfs datasets +##################################### - if [[ "${#label}" -gt "${limit}" ]]; then - echo >&2 "label is longer than ${limit}: ${label}" - return 1 - fi +# check if a dataset is bound to a specific label (or any label) +function zfs_is_bound() { + local dataset="${1}" + local label="${2:-}" - if [[ "${label}" =~ ${regex} ]]; then + if [[ -z "${label}" ]] && [[ -n "$(zfs_get_labels "${dataset}")" ]]; then + return 0 + elif [[ -n "$(zfs_get_label "${dataset}" "${label}")" ]]; then return 0 else - echo >&2 "label is invalid: '${label}'. Expecting an alphanumeric string " return 1 fi } +# we can only load keys on encryptionroots +# it does not make sense to add a binding elsewhere +function zfs_is_encryptionroot() { + local dataset="${1}" + [[ "$(zfs_get_property "${dataset}" 'encryptionroot' -snone )" == "${dataset}" ]] +} + +# does it even exist? +function zfs_is_dataset() { + zfs_get_property "${dataset}" 'name' -snone &>/dev/null +} + + +function check_valid_dataset() { + local dataset="${1}" + + if ! zfs_is_dataset; then + error "${dataset} is not a zfs dataset!" + fi + + if ! zfs_is_encryptionroot "${dataset}"; then + error "given dataset is not an encryptionroot: ${dataset}" + fi +} + -function read_key() { +# functions to deal with I/O to the user +function read_passphrase() { local dataset="${1}" local key="${2?need keyinput argument}" @@ -105,63 +205,29 @@ function error() { exit 1 } -findexe() { - while read -r -d: path; do - [ -f "${path}/${1}" ] && [ -x "${path}/${1}" ] && \ - echo "${path}/${1}" && return 0 - done <<< "${PATH}:" - return 1 -} -zfs_get_prop() { - local dataset="${1}" - local prop="${2}" - shift 2 - zfs get "${prop}" "${dataset}" -H -o value -slocal "${@}" -} +# functions to deal with too large clevis data for a single ZFS property +######################################################################## function cut_into_chunks() { - fold -w "${zfs_userprop_max_size}" -} - -function zfs_is_bound() { - local dataset="${1}" - local label_to_check="${2:-}" - local label="${label_to_check%:*}" - - local labels="$(zfs_get_labels "${dataset}")" - if [[ -n "${label}" ]]; then - [[ " ${labels} " == *" ${label} "* ]] && return 0 - else - # we don't check for a specific label, just that at least one label is set - [[ "${#labels}" -gt 0 ]] && return 0 - fi - return 1 + fold -w "${zfs_userprop_value_limit}" } -function zfs_is_encryptionroot() { - local dataset="${1}" - [[ "$(zfs_get_prop "${dataset}" 'encryptionroot' -snone )" == "${dataset}" ]] -} - -function zfs_test_key() { - zfs_load_key "${1}" 'dry_run' +function zero_pad() { + local width="${1}"; shift + printf "%0${width}d " "${@}" } -function zfs_load_key() { - local dataset="${1}" - local dry_run="${2:+-n}" - zfs load-key ${dry_run} -L prompt "${dataset}" >/dev/null +function num_list() { + local last_index="${1}" + zero_pad "${#last_index}" $(eval "echo {0..${last_index}}") } -function zero_pad() { - local num="${1}" - local width="${2}" - printf "%0${width}d" "${num}" -} -function zfs_bind_clevis_data() { +# functions to add/remove a clevis binding +######################################### +function zfs_bind_clevis_label() { local dataset="${1}" local label="${2}" local clevis_data="${3}" @@ -170,22 +236,22 @@ function zfs_bind_clevis_data() { echo >&2 -n 'binding new clevis data... ' # use a single prop without number suffix if it will fit in one prop - if [[ "${#clevis_data}" -lt "${zfs_userprop_max_size}" ]]; then - zfs set "${zfs_label_prop}=${clevis_data}" "${dataset}" + if [[ "${#clevis_data}" -lt "${zfs_userprop_value_limit}" ]]; then + zfs_set_property "${dataset}" "${zfs_label_prop}" "${clevis_data}" else clevis_chunks=( $(cut_into_chunks <<<"${clevis_data}") ) last_index="$(( "${#clevis_chunks[@]}" - 1 ))" width="${#last_index}" local chunk chunk_num - for i in $(seq 0 "${last_index}" ); do + for chunk_num in $(num_list "${last_index}"); do + # bash assumes numbers are octal when prefixed with a 0, so we + # remove it + i="${chunk_num##0}" + i="${i:-0}" # if we removed all zeroes, we are at the start chunk="${clevis_chunks[${i}]}" - # we add zero-padding so the props sort nicely when we want to combine - # them when we unlock - chunk_num="$(zero_pad "${i}" "${width}" )" - # e.g. latchset.clevis.label:${label}-01=chunk_data - zfs set "${zfs_label_prop}-${chunk_num}=${chunk}" "${dataset}" + zfs_set_property "${dataset}" "${zfs_label_prop}-${chunk_num}" "${chunk}" done label="${label}:${last_index}" @@ -194,31 +260,41 @@ function zfs_bind_clevis_data() { # check if unlocking works echo >&2 -n 'testing new clevis data... ' - if ! (zfs_get_clevis_label "${dataset}" "${label}" | clevis decrypt | zfs_test_key "${dataset}"); then - zfs_wipe_clevis_label "${dataset}" "${label}" + + # somehow clevis-decrypt exits with a non-zero code, but still outputs the + # correct data, so we ignore the exit code. zfs_test_key will fail anyway + # if something goes wrong + if ! unlock_with_label "${dataset}" "${label}" 'dry_run'; then + zfs_unbind_clevis_label "${dataset}" "${label}" error "could not unlock dataset with clevis configuration: ${dataset}" fi + echo >&2 'ok' zfs_add_label "${dataset}" "${label}" } -function zfs_wipe_clevis_label() { +function zfs_unbind_clevis_label() { local dataset="${1}" local label="${2%:*}" - local last_index="${2#*:}" + local last_index + + label="$(zfs_get_label "${dataset}" "${label}")" + last_index="${label#*:}" + label="${label%:*}" local zfs_label_prop="${zfs_label_prefix}:${label}" if [[ "${label}" == "${last_index}" ]]; then - zfs inherit "${zfs_label_prop}" "${dataset}" + zfs_remove_property "${dataset}" "${zfs_label_prop}" else - for num in $(seq -w 0 "${last_index}"); do - zfs inherit "${zfs_label_prop}-${num}" "${dataset}" + for num in $(num_list "${last_index}"); do + zfs_remove_property "${dataset}" "${zfs_label_prop}-${num}" done fi zfs_remove_label "${dataset}" "${label}" } + function zfs_get_clevis_label() { local dataset="${1}" local label="${2%:*}" @@ -226,15 +302,32 @@ function zfs_get_clevis_label() { local zfs_label_prop="${zfs_label_prefix}:${label}" if [[ "${label}" == "${last_index}" ]]; then - zfs_get_prop "${dataset}" "${zfs_label_prop}" + zfs_get_property "${dataset}" "${zfs_label_prop}" else - for num in $(seq -w 0 "${last_index}"); do - zfs_get_prop "${dataset}" "${zfs_label_prop}-${num}" | tr -d '\n' + clevis_data=() + for num in $(num_list "${last_index}"); do + clevis_data+=( $(zfs_get_property "${dataset}" "${zfs_label_prop}-${num}") ) done + local IFS='' + echo "${clevis_data[*]}" fi } +function unlock_with_label(){ + local dataset="${1}" + local label="${2}" + local test_only="${3:-}" + + # somehow clevis-decrypt exits with a non-zero code, but still outputs the + # correct data, so we ignore the exit code. zfs_load_key will fail anyway + # if something goes wrong + zfs_get_clevis_label "${dataset}" "${label}" \ + | (clevis decrypt || true) \ + | zfs_load_key "${dataset}" "${test_only}" +} + + if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then . clevis-zfs-test _test "$@" diff --git a/src/zfs/clevis-zfs-test b/src/zfs/clevis-zfs-test index d69855a6..eaef63d6 100755 --- a/src/zfs/clevis-zfs-test +++ b/src/zfs/clevis-zfs-test @@ -7,7 +7,33 @@ test_dataset="${root_dataset}/test" tang_host='127.0.0.1' tang_thp='TANG_THP' + +tang_host="192.168.178.26:8565" +tang_thp="NHt3FdBvUX0AiHZycp2emEzfYIc" + +shutup='/dev/null' + +# simple tang config tang_config='{"url": "http://'${tang_host}'", "thp": "'${tang_thp}'"}' +# config that will go over the 8k limit +sss_config='{ + "t": 4, + "pins": { + "tang": ['"${tang_config}","${tang_config}","${tang_config}"'], + "sss": { + "t": 4, + "pins": { + "tang": ['"${tang_config}","${tang_config}","${tang_config}"'], + "sss": { + "t": 3, + "pins": { + "tang": ['"${tang_config}","${tang_config}","${tang_config}"'] + } + } + } + } + } +}' function zfs_usage() { local permissions='create,destroy,mount,load-key,change-key,userprop,encryption,keyformat,keylocation' @@ -51,8 +77,9 @@ function failed() { function _test() { _test_is_valid_label - _test_read_key + _test_read_passphrase _test_zero_pad + _test_num_list _test_zfs_functions [[ "${exit_code}" -gt 0 ]] && echo >&2 "SOME TESTS HAVE FAILED" exit "${exit_code}" @@ -95,7 +122,7 @@ function _test_is_valid_label() { expected=0 result=1 for l in "${valid_labels[@]}"; do - if ! is_valid_label "${l}" &>/dev/null; then + if ! is_valid_label "${l}" &>${shutup}; then failed "for is_valid_label '${l}'" fi done @@ -105,7 +132,7 @@ function _test_is_valid_label() { expected=1 result=0 for l in "${invalid_labels[@]}"; do - if is_valid_label "${l}" &>/dev/null; then + if is_valid_label "${l}" &>${shutup}; then failed "for \`is_valid_label '${l}'\`" fi done @@ -113,40 +140,46 @@ function _test_is_valid_label() { } -function _test_read_key() { +function _test_read_passphrase() { # test with reading from stdin local expected local result testing 'key test: no key argument (stdin)' expected='no-argument-password' - result="$(read_key "mydataset" '' <<<"${expected}" 2>/dev/null)" + result="$(read_passphrase "mydataset" '' <<<"${expected}" 2>/dev/null)" [[ "${expected}" == "${result}" ]] && success || failed testing 'key test: dash argument (stdin)' expected='dash-argument-password' - result="$(read_key "mydataset" '-' <<<"${expected}" 2>/dev/null)" + result="$(read_passphrase "mydataset" '-' <<<"${expected}" 2>/dev/null)" [[ "${expected}" == "${result}" ]] && success || failed testing 'key arg: filename' expected='filename-argument-password' - result="$(read_key "mydataset" <(echo "${expected}") 2>/dev/null)" + result="$(read_passphrase "mydataset" <(echo "${expected}") 2>/dev/null)" [[ "${expected}" == "${result}" ]] && success || failed } function _test_zero_pad() { testing 'pad with 4 zeroes' - local expected='000051' - local result="$(zero_pad 51 6)" + local expected='000051 ' + local result="$(zero_pad 6 51)" [[ "${expected}" == "${result}" ]] && success || failed } +function _test_num_list() { + testing 'list with zero padding' + local expected='00 01 02 03 04 05 06 07 08 09 10 ' + local result="$(num_list 10)" + [[ "${expected}" == "${result}" ]] && success || failed +} function _zfs_test_teardown() { testing "removing zfs testing dataset: ${test_dataset}" - ! zfs list "${test_dataset}" &>/dev/null && success && return 0 + ! zfs list "${test_dataset}" &>${shutup} && success && return 0 zfs destroy -f -r "${test_dataset}" success } @@ -156,34 +189,44 @@ function _zfs_create_encrypted_dataset() { testing "creating encrypted test dataset: ${1}" # zfs create will work with the permissions as described, but will exit # with an error code because mounting failed - zfs create "${1}" -o encryption=on -o keyformat=passphrase <<<"${testing_password}" &>/dev/null || true + zfs create "${1}" -o encryption=on -o keyformat=passphrase <<<"${testing_password}" &>${shutup} || true # we need to make sure the dataset is created because of the error-code shenanigans - zfs list "${1}" &>/dev/null && success || return 1 + zfs list "${1}" &>${shutup} && success || return 1 } function _test_zfs_functions() { testing "checking if zfs testing dataset exists: ${root_dataset}" - zfs list "${root_dataset}" &>/dev/null && success || (zfs_usage; return 1) + zfs list "${root_dataset}" &>${shutup} && success || (zfs_usage; return 1) trap _zfs_test_teardown EXIT _zfs_create_encrypted_dataset "${test_dataset}" testing 'binding zfs dataset twice' - echo "${testing_password}" | ./clevis-zfs-bind -d "${test_dataset}" -k - -l 'testlabel' tang "${tang_config}" &>/dev/null - echo "${testing_password}" | ./clevis-zfs-bind -d "${test_dataset}" -k - -l 'another_testlabel' tang "${tang_config}" &>/dev/null + echo "${testing_password}" | ./clevis-zfs-bind -d "${test_dataset}" -k - -l 'tang_testlabel' tang "${tang_config}" &>/dev/null + echo "${testing_password}" | ./clevis-zfs-bind -d "${test_dataset}" -k - -l 'sss_testlabel' sss "${sss_config}" &>/dev/null success testing 'listing labels' - expected="${test_dataset}"$'\ttestlabel:1 another_testlabel:1' + # the tang_testlabel should be way under the 8k limit + # the sss_testlabel should be over 16k + expected="${test_dataset}"$'\ttang_testlabel sss_testlabel:2' result="$(./clevis-zfs-list -d "${test_dataset}" 2>/dev/null)" [[ "${expected}" == "${result}" ]] && success || failed - testing 'testing unlocking with bindings' - ./clevis-zfs-unlock -t -d "${test_dataset}" && success + expected='unlock test success' + result='unlock test failure' + #shutup='/dev/stderr' + testing 'testing unlocking with bindings tang' + ./clevis-zfs-unlock -t -d "${test_dataset}" -l 'tang_testlabel' &>${shutup} && success || failed + return + testing 'testing unlocking with bindings sss' + ./clevis-zfs-unlock -t -d "${test_dataset}" -l 'sss_testlabel' &>${shutup} && success || failed + testing 'testing unlocking with bindings either' + ./clevis-zfs-unlock -t -d "${test_dataset}" &>${shutup} && success || failed testing 'unlocking with binding' zfs unload-key "${test_dataset}" - [[ "$(zfs_get_prop "${test_dataset}" 'keystatus' -snone)" == 'unavailable' ]] || return 1 - ./clevis-zfs-unlock -d "${test_dataset}" + [[ "$(zfs_get_property "${test_dataset}" 'keystatus' -snone)" == 'unavailable' ]] || return 1 + ./clevis-zfs-unlock -d "${test_dataset}" &>${shutup} success testing 'unbinding dataset twice' diff --git a/src/zfs/clevis-zfs-unbind b/src/zfs/clevis-zfs-unbind index 1ca0bc8c..b3146685 100755 --- a/src/zfs/clevis-zfs-unbind +++ b/src/zfs/clevis-zfs-unbind @@ -57,7 +57,7 @@ function main() { fi echo >&2 -n 'wiping clevis data... ' - zfs_wipe_clevis_label "${dataset}" "${label}" + zfs_unbind_clevis_label "${dataset}" "${label}" echo >&2 'ok' } diff --git a/src/zfs/clevis-zfs-unlock b/src/zfs/clevis-zfs-unlock index b00be846..621d4fd9 100755 --- a/src/zfs/clevis-zfs-unlock +++ b/src/zfs/clevis-zfs-unlock @@ -11,11 +11,13 @@ function usage() { -t Test the clevis configuration without unlocking -d DATASET The zfs dataset to unlock - -l LABEL Use only this label to unlock (defaults to trying all labels): TODO + -l LABEL Use only this label to unlock (defaults to trying all labels) USAGE_END } + + function main() { if [ $# -eq 1 -a "${1:-}" == "--summary" ]; then echo "$SUMMARY" @@ -42,24 +44,42 @@ function main() { error "dataset is not bound with clevis: ${dataset}" fi - local clevis_data password - - for label in $(zfs_get_labels "${dataset}" | tr ' ' '\n' | grep "^${label}(:|$)"); do - echo >&2 -n "loading clevis data from label ${label}... " - clevis_data="$(zfs_get_clevis_label "${dataset}" "${label}")" - password="$(clevis decrypt <<<"${clevis_data}" || echo '' )" - echo >&2 'ok' + local clevis_data password labels - echo >&2 -n "unlocking ${dataset}${test_only}... " - if echo "${password}" | zfs_load_key "${dataset}" "${test_only}"; then - echo >&2 'ok' - [[ -z "${test_only}" ]] && exit 0 + if [[ -n "${label}" ]]; then + label="$(zfs_get_label "${dataset}" "${label}" )" + testing -n "unlocking ${dataset} with ${label} ${test_only}... " + if unlock_with_label "${dataset}" "${label}" "${test_only}"; then + testing 'ok' + exit 0 else - echo >&2 "failed" - continue + testing 'failed' + exit 1 fi - done - exit 1 + else + labels="$(zfs_get_labels "${dataset}")" + for label in ${labels}; do + testing -n "unlocking ${dataset} with ${label} ${test_only}... " + if unlock_with_label "${dataset}" "${label}" "${test_only}"; then + testing 'ok' + [[ -z "${test_only}" ]] && exit 0 || true + else + testing "failed" + continue + fi + done + fi + + if [[ -n "${test_only}" ]]; then + exit 0 + else + exit 1 + fi +} + + +function testing() { + [[ -n "${test_only}" ]] && echo >&2 "${@}" } if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then From ae345c6018954ee8ffcc3c44226d5465a79c87f3 Mon Sep 17 00:00:00 2001 From: Vince van Oosten Date: Thu, 26 May 2022 15:25:04 +0200 Subject: [PATCH 07/20] more fixes --- src/zfs/clevis-zfs-list | 6 ++++-- src/zfs/clevis-zfs-test | 36 +++++++++++++++++++----------------- src/zfs/clevis-zfs-unbind | 2 +- src/zfs/clevis-zfs-unlock | 7 +++---- 4 files changed, 27 insertions(+), 24 deletions(-) diff --git a/src/zfs/clevis-zfs-list b/src/zfs/clevis-zfs-list index f6e095ae..b65a1726 100755 --- a/src/zfs/clevis-zfs-list +++ b/src/zfs/clevis-zfs-list @@ -16,7 +16,7 @@ function usage() { main() { - if [ $# -eq 1 -a "${1:-}" == "--summary" ]; then + if [ $# -eq 1 ] && [ "${1:-}" == "--summary" ]; then echo "$SUMMARY" exit 0 fi @@ -30,7 +30,9 @@ main() { done echo >&2 "The following ZFS datasets have been bound with clevis:" - zfs get -r -H -o name,value -slocal "${zfs_labels_prop}" ${dataset} + # we should not quote this in case it is empty + # shellcheck disable=SC2086 + zfs get -r -H -o name,value -slocal "${zfs_labels_prop}" ${dataset:-} } main "${@}" diff --git a/src/zfs/clevis-zfs-test b/src/zfs/clevis-zfs-test index eaef63d6..5a69e472 100755 --- a/src/zfs/clevis-zfs-test +++ b/src/zfs/clevis-zfs-test @@ -15,7 +15,7 @@ shutup='/dev/null' # simple tang config tang_config='{"url": "http://'${tang_host}'", "thp": "'${tang_thp}'"}' -# config that will go over the 8k limit +# config that will go over the 8k limit twice (i.e. >16k) sss_config='{ "t": 4, "pins": { @@ -186,12 +186,12 @@ function _zfs_test_teardown() { function _zfs_create_encrypted_dataset() { local dataset="${1}" - testing "creating encrypted test dataset: ${1}" + testing "creating encrypted test dataset: ${dataset}" # zfs create will work with the permissions as described, but will exit # with an error code because mounting failed - zfs create "${1}" -o encryption=on -o keyformat=passphrase <<<"${testing_password}" &>${shutup} || true + zfs create "${dataset}" -o encryption=on -o keyformat=passphrase <<<"${testing_password}" &>${shutup} || true # we need to make sure the dataset is created because of the error-code shenanigans - zfs list "${1}" &>${shutup} && success || return 1 + zfs list "${dataset}" &>${shutup} && success || return 1 } function _test_zfs_functions() { @@ -200,10 +200,10 @@ function _test_zfs_functions() { trap _zfs_test_teardown EXIT _zfs_create_encrypted_dataset "${test_dataset}" - testing 'binding zfs dataset twice' - echo "${testing_password}" | ./clevis-zfs-bind -d "${test_dataset}" -k - -l 'tang_testlabel' tang "${tang_config}" &>/dev/null - echo "${testing_password}" | ./clevis-zfs-bind -d "${test_dataset}" -k - -l 'sss_testlabel' sss "${sss_config}" &>/dev/null - success + testing 'binding zfs dataset tang' + echo "${testing_password}" | ./clevis-zfs-bind -d "${test_dataset}" -k - -l 'tang_testlabel' tang "${tang_config}" &>${shutup} && success || failed + testing 'binding zfs dataset sss' + echo "${testing_password}" | ./clevis-zfs-bind -d "${test_dataset}" -k - -l 'sss_testlabel' sss "${sss_config}" &>${shutup} && success || failed testing 'listing labels' # the tang_testlabel should be way under the 8k limit @@ -214,23 +214,25 @@ function _test_zfs_functions() { expected='unlock test success' result='unlock test failure' - #shutup='/dev/stderr' testing 'testing unlocking with bindings tang' ./clevis-zfs-unlock -t -d "${test_dataset}" -l 'tang_testlabel' &>${shutup} && success || failed - return + testing 'testing unlocking with bindings sss' ./clevis-zfs-unlock -t -d "${test_dataset}" -l 'sss_testlabel' &>${shutup} && success || failed testing 'testing unlocking with bindings either' ./clevis-zfs-unlock -t -d "${test_dataset}" &>${shutup} && success || failed testing 'unlocking with binding' - zfs unload-key "${test_dataset}" - [[ "$(zfs_get_property "${test_dataset}" 'keystatus' -snone)" == 'unavailable' ]] || return 1 - ./clevis-zfs-unlock -d "${test_dataset}" &>${shutup} + expected='unlock success' + zfs unload-key "${test_dataset}" || result='unload key failed' failed + [[ "$(zfs_get_property "${test_dataset}" 'keystatus' -snone)" == 'unavailable' ]] || result='unload key failed' failed + ./clevis-zfs-unlock -d "${test_dataset}" &>${shutup} || result='unlocking failed' failed success - testing 'unbinding dataset twice' - echo "${testing_password}" | ./clevis-zfs-unbind -d "${test_dataset}" -l 'another_testlabel' -k - - echo "${testing_password}" | ./clevis-zfs-unbind -d "${test_dataset}" -l 'testlabel' -k - - success + expected='unbinding success' + result='unbinding failed' + testing 'unbinding dataset tang' + echo "${testing_password}" | ./clevis-zfs-unbind -d "${test_dataset}" -l 'tang_testlabel' -k - &>${shutup} && success || failed + testing 'unbinding dataset sss' + echo "${testing_password}" | ./clevis-zfs-unbind -d "${test_dataset}" -l 'sss_testlabel' -k - &>${shutup} && success || failed } diff --git a/src/zfs/clevis-zfs-unbind b/src/zfs/clevis-zfs-unbind index b3146685..35079f49 100755 --- a/src/zfs/clevis-zfs-unbind +++ b/src/zfs/clevis-zfs-unbind @@ -50,7 +50,7 @@ function main() { local existing_key echo >&2 "Loading existing key... " - existing_key="$(read_key "${dataset}" "${key}")" + existing_key="$(read_passphrase "${dataset}" "${key}")" if ! zfs_test_key "${dataset}" <<<"${existing_key}"; then error "given key does not unlock ${dataset}" diff --git a/src/zfs/clevis-zfs-unlock b/src/zfs/clevis-zfs-unlock index 621d4fd9..03149df3 100755 --- a/src/zfs/clevis-zfs-unlock +++ b/src/zfs/clevis-zfs-unlock @@ -19,7 +19,7 @@ function usage() { function main() { - if [ $# -eq 1 -a "${1:-}" == "--summary" ]; then + if [ $# -eq 1 ] && [ "${1:-}" == "--summary" ]; then echo "$SUMMARY" exit 0 fi @@ -44,8 +44,6 @@ function main() { error "dataset is not bound with clevis: ${dataset}" fi - local clevis_data password labels - if [[ -n "${label}" ]]; then label="$(zfs_get_label "${dataset}" "${label}" )" testing -n "unlocking ${dataset} with ${label} ${test_only}... " @@ -57,6 +55,7 @@ function main() { exit 1 fi else + local labels labels="$(zfs_get_labels "${dataset}")" for label in ${labels}; do testing -n "unlocking ${dataset} with ${label} ${test_only}... " @@ -79,7 +78,7 @@ function main() { function testing() { - [[ -n "${test_only}" ]] && echo >&2 "${@}" + [[ -n "${test_only}" ]] && echo >&2 "${@}" || true } if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then From 8ff6b23eba41103b2101a55ce1525a43a8255c35 Mon Sep 17 00:00:00 2001 From: Vince van Oosten Date: Sun, 4 Dec 2022 10:49:56 +0100 Subject: [PATCH 08/20] misc changes --- src/initramfs-tools/hooks/clevis.in | 1 + src/initramfs-tools/scripts/local-bottom/meson.build | 6 ++++++ src/initramfs-tools/scripts/local-top/meson.build | 6 ++++++ src/zfs/clevis-zfs-bind | 2 +- src/zfs/clevis-zfs-test | 12 +++++++++--- src/zfs/clevis-zfs-unbind | 6 +++--- 6 files changed, 26 insertions(+), 7 deletions(-) diff --git a/src/initramfs-tools/hooks/clevis.in b/src/initramfs-tools/hooks/clevis.in index 3d4eb67f..1b406fdc 100755 --- a/src/initramfs-tools/hooks/clevis.in +++ b/src/initramfs-tools/hooks/clevis.in @@ -61,6 +61,7 @@ copy_exec @bindir@/clevis-decrypt-sss || die 1 "@bindir@/clevis-decrypt-sss not copy_exec @bindir@/clevis-decrypt-null || die 1 "@bindir@/clevis-decrypt-null not found" copy_exec @bindir@/clevis-decrypt || die 1 "@bindir@/clevis-decrypt not found" copy_exec @bindir@/clevis-luks-common-functions || die 1 "@bindir@/clevis-luks-common-functions not found" +copy_exec @bindir@/clevis-zfs-common || die 1 "@bindir@/clevis-zfs-common not found" copy_exec @bindir@/clevis-luks-list || die 1 "@bindir@/clevis-luks-list not found" if [ -x @bindir@/clevis-decrypt-tpm2 ]; then copy_exec @bindir@/clevis-decrypt-tpm2 || die 1 "@bindir@/clevis-decrypt-tpm2 not found" diff --git a/src/initramfs-tools/scripts/local-bottom/meson.build b/src/initramfs-tools/scripts/local-bottom/meson.build index 68d3becb..5fa9ff4b 100644 --- a/src/initramfs-tools/scripts/local-bottom/meson.build +++ b/src/initramfs-tools/scripts/local-bottom/meson.build @@ -4,3 +4,9 @@ configure_file( install_dir: join_paths(initramfs_scripts_dir, 'local-bottom'), configuration: initramfs_data, ) +configure_file( + input: 'clevis-zfs.in', + output: 'clevis-zfs', + install_dir: join_paths(initramfs_scripts_dir, 'local-bottom'), + configuration: initramfs_data, +) diff --git a/src/initramfs-tools/scripts/local-top/meson.build b/src/initramfs-tools/scripts/local-top/meson.build index 38fca25a..d0f5df0f 100644 --- a/src/initramfs-tools/scripts/local-top/meson.build +++ b/src/initramfs-tools/scripts/local-top/meson.build @@ -4,3 +4,9 @@ configure_file( install_dir: join_paths(initramfs_scripts_dir, 'local-top'), configuration: initramfs_data, ) +configure_file( + input: 'clevis-zfs.in', + output: 'clevis-zfs', + install_dir: join_paths(initramfs_scripts_dir, 'local-top'), + configuration: initramfs_data, +) diff --git a/src/zfs/clevis-zfs-bind b/src/zfs/clevis-zfs-bind index c274d5af..520aa56d 100755 --- a/src/zfs/clevis-zfs-bind +++ b/src/zfs/clevis-zfs-bind @@ -70,7 +70,7 @@ function main() { local pin local cfg local label - local key + local key='' local overwrite='' while getopts ":hfd:l:k:" o; do case "$o" in diff --git a/src/zfs/clevis-zfs-test b/src/zfs/clevis-zfs-test index 5a69e472..096419fe 100755 --- a/src/zfs/clevis-zfs-test +++ b/src/zfs/clevis-zfs-test @@ -187,11 +187,17 @@ function _zfs_test_teardown() { function _zfs_create_encrypted_dataset() { local dataset="${1}" testing "creating encrypted test dataset: ${dataset}" - # zfs create will work with the permissions as described, but will exit - # with an error code because mounting failed + # zfs create will work, assuming we have the permissions as described in + # zfs_usage, but will exit with an error code because only the root user + # can mount the dataset zfs create "${dataset}" -o encryption=on -o keyformat=passphrase <<<"${testing_password}" &>${shutup} || true # we need to make sure the dataset is created because of the error-code shenanigans - zfs list "${dataset}" &>${shutup} && success || return 1 + if zfs list "${dataset}" &>${shutup}; then + success + return 0 + else + return 1 + fi } function _test_zfs_functions() { diff --git a/src/zfs/clevis-zfs-unbind b/src/zfs/clevis-zfs-unbind index 35079f49..206c96a2 100755 --- a/src/zfs/clevis-zfs-unbind +++ b/src/zfs/clevis-zfs-unbind @@ -28,9 +28,9 @@ function main() { exit 0 fi - local dataset - local key - local label + local dataset= + local key= + local label= while getopts ":hfd:k:l:" o; do case "$o" in d) dataset="$OPTARG";; From 39cf8a46b2d54f0dbeec806beee6bb101d76d406 Mon Sep 17 00:00:00 2001 From: Vince van Oosten Date: Sun, 4 Dec 2022 10:59:18 +0100 Subject: [PATCH 09/20] add dracut module that works on _my_ debian machine --- dracut/60clevis-zfs/clevis-zfs-hook.sh | 53 ++++++++++++++++++++++++++ dracut/60clevis-zfs/module-setup.sh | 42 ++++++++++++++++++++ 2 files changed, 95 insertions(+) create mode 100755 dracut/60clevis-zfs/clevis-zfs-hook.sh create mode 100755 dracut/60clevis-zfs/module-setup.sh diff --git a/dracut/60clevis-zfs/clevis-zfs-hook.sh b/dracut/60clevis-zfs/clevis-zfs-hook.sh new file mode 100755 index 00000000..ab3b0f5f --- /dev/null +++ b/dracut/60clevis-zfs/clevis-zfs-hook.sh @@ -0,0 +1,53 @@ +#!/bin/bash + + +# import the libs now that we know the pool imported +[ -f /lib/dracut-lib.sh ] && dracutlib=/lib/dracut-lib.sh +[ -f /usr/lib/dracut/modules.d/99base/dracut-lib.sh ] && dracutlib=/usr/lib/dracut/modules.d/99base/dracut-lib.sh +# shellcheck source=./lib-zfs.sh.in +. "$dracutlib" + +# load the kernel command line vars +[ -z "$root" ] && root="$(getarg root=)" +# If root is not ZFS= or zfs: or rootfstype is not zfs then we are not supposed to handle it. +[ "${root##zfs:}" = "${root}" ] && [ "${root##ZFS=}" = "${root}" ] && [ "$rootfstype" != "zfs" ] && exit 0 + +# There is a race between the zpool import and the pre-mount hooks, so we wait for a pool to be imported +while true; do + zpool list -H | grep -q -v '^$' && break + [ "$(systemctl is-failed zfs-import-cache.service)" = 'failed' ] && exit 1 + [ "$(systemctl is-failed zfs-import-scan.service)" = 'failed' ] && exit 1 + sleep 0.1s +done + +# run this after import as zfs-import-cache/scan service is confirmed good +# we do not overwrite the ${root} variable, but create a new one, BOOTFS, to hold the dataset +if [ "${root}" = "zfs:AUTO" ] ; then + BOOTFS="$(zpool list -H -o bootfs | awk '$1 != "-" {print; exit}')" +else + BOOTFS="${root##zfs:}" + BOOTFS="${BOOTFS##ZFS=}" +fi + +# if pool encryption is active and the zfs command understands '-o encryption' +if [ "$(zpool list -H -o feature@encryption $(echo "${BOOTFS}" | awk -F\/ '{print $1}'))" = 'active' ]; then + # if the root dataset has encryption enabled + ENCRYPTIONROOT=$(zfs get -H -o value encryptionroot "${BOOTFS}") + # where the key is stored (in a file or loaded via prompt) + KEYLOCATION=$(zfs get -H -o value keylocation "${ENCRYPTIONROOT}") + if ! [ "${ENCRYPTIONROOT}" = "-" ]; then + KEYSTATUS="$(zfs get -H -o value keystatus "${ENCRYPTIONROOT}")" + # continue only if the key needs to be loaded + [ "$KEYSTATUS" = "unavailable" ] || exit 0 + # decrypt them + TRY_COUNT=5 + while [ $TRY_COUNT -gt 0 ]; do + echo >&2 "Attempting to unlock with clevis-zfs-unlock; ${TRY_COUNT} attempts left..." + clevis-zfs-unlock -d "${ENCRYPTIONROOT}" && break + TRY_COUNT=$((TRY_COUNT - 1)) + done + fi +fi + + + diff --git a/dracut/60clevis-zfs/module-setup.sh b/dracut/60clevis-zfs/module-setup.sh new file mode 100755 index 00000000..4212c4ea --- /dev/null +++ b/dracut/60clevis-zfs/module-setup.sh @@ -0,0 +1,42 @@ +#!/bin/bash +# vim: set tabstop=8 shiftwidth=4 softtabstop=4 expandtab smarttab colorcolumn=80: +# +# Copyright (c) 2016 Red Hat, Inc. +# Author: Nathaniel McCallum +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# + +depends() { + # do we have a hard dependency on systemd? + echo zfs systemd + return 255 +} + +install() { + inst_multiple \ + /etc/services \ + grep sed cut \ + clevis-decrypt \ + clevis-zfs-common \ + clevis-zfs-unlock \ + clevis-zfs-list \ + clevis \ + mktemp \ + jose + + inst_hook pre-mount 90 "${moddir}/clevis-zfs-hook.sh" + + dracut_need_initqueue +} From db27dc134d427df8b293bf4844a56947f1b02984 Mon Sep 17 00:00:00 2001 From: Vince van Oosten Date: Sun, 4 Dec 2022 11:00:35 +0100 Subject: [PATCH 10/20] misc updates --- src/zfs/clevis-zfs-common | 2 +- src/zfs/clevis-zfs-list | 10 ++++++++-- src/zfs/clevis-zfs-unbind | 40 ++++++++++++++++++++++++++++----------- 3 files changed, 38 insertions(+), 14 deletions(-) diff --git a/src/zfs/clevis-zfs-common b/src/zfs/clevis-zfs-common index 28a15572..5db5a2bc 100755 --- a/src/zfs/clevis-zfs-common +++ b/src/zfs/clevis-zfs-common @@ -202,7 +202,7 @@ function read_passphrase() { function error() { usage echo >&2 -e "ERROR: ${*}" - exit 1 + exit 1 } diff --git a/src/zfs/clevis-zfs-list b/src/zfs/clevis-zfs-list index b65a1726..9777b6f8 100755 --- a/src/zfs/clevis-zfs-list +++ b/src/zfs/clevis-zfs-list @@ -29,10 +29,16 @@ main() { esac done + if [ -n "${dataset}" ]; then + output='value' + else + output='name,value' + fi + echo >&2 "The following ZFS datasets have been bound with clevis:" - # we should not quote this in case it is empty + # we should not quote ${dataset:-} in case it is empty # shellcheck disable=SC2086 - zfs get -r -H -o name,value -slocal "${zfs_labels_prop}" ${dataset:-} + zfs get -H -o "${output}" -slocal "${zfs_labels_prop}" ${dataset:-} } main "${@}" diff --git a/src/zfs/clevis-zfs-unbind b/src/zfs/clevis-zfs-unbind index 206c96a2..bbc110e2 100755 --- a/src/zfs/clevis-zfs-unbind +++ b/src/zfs/clevis-zfs-unbind @@ -31,12 +31,17 @@ function main() { local dataset= local key= local label= - while getopts ":hfd:k:l:" o; do + local force_unbind='false' + local unbind_all='false' + while getopts "hafd:k:l:" o; do case "$o" in + h) usage; exit 0;; + a) unbind_all='true';; d) dataset="$OPTARG";; + f) force_unbind='true';; k) key="$OPTARG";; l) label="$OPTARG";; - *) error "unrecognized argument: -${OPTARG}";; + *) error "unrecognized argument: -${o}";; esac done @@ -44,21 +49,34 @@ function main() { error "did not specify a device!" fi - if ! zfs_is_bound "${dataset}"; then - error "dataset is not bound with clevis: ${dataset}" - fi + if [[ "${force_unbind}" != 'true' ]]; then + + if ! zfs_is_bound "${dataset}"; then + error "dataset is not bound with clevis: ${dataset}" + fi - local existing_key - echo >&2 "Loading existing key... " - existing_key="$(read_passphrase "${dataset}" "${key}")" + local existing_key + echo >&2 "Loading existing key... " + existing_key="$(read_passphrase "${dataset}" "${key}")" - if ! zfs_test_key "${dataset}" <<<"${existing_key}"; then - error "given key does not unlock ${dataset}" + if ! zfs_test_key "${dataset}" <<<"${existing_key}"; then + error "given key does not unlock ${dataset}" + fi fi + + echo >&2 -n 'wiping clevis data... ' - zfs_unbind_clevis_label "${dataset}" "${label}" + if [[ "${unbind_all}" == 'true' ]]; then + labels="$(zfs_get_labels "${dataset}")" + for label in ${labels}; do + zfs_unbind_clevis_label "${dataset}" "${label}" + done + else + zfs_unbind_clevis_label "${dataset}" "${label}" + fi echo >&2 'ok' + } main "${@}" From 7f3e6247600296cf8beaa6c5a076d585a7f9396a Mon Sep 17 00:00:00 2001 From: Joel Low Date: Tue, 11 Jun 2024 14:53:54 +0800 Subject: [PATCH 11/20] Fix lints and remove missing clevis-zfs.in --- .../scripts/local-bottom/meson.build | 6 --- .../scripts/local-top/meson.build | 6 --- src/zfs/clevis-zfs-bind | 48 +++++++++---------- src/zfs/clevis-zfs-common | 43 +++++++++-------- src/zfs/clevis-zfs-list | 26 +++++----- src/zfs/clevis-zfs-test | 10 ++-- src/zfs/clevis-zfs-unbind | 34 ++++++------- src/zfs/clevis-zfs-unlock | 27 +++++------ 8 files changed, 95 insertions(+), 105 deletions(-) diff --git a/src/initramfs-tools/scripts/local-bottom/meson.build b/src/initramfs-tools/scripts/local-bottom/meson.build index 5fa9ff4b..68d3becb 100644 --- a/src/initramfs-tools/scripts/local-bottom/meson.build +++ b/src/initramfs-tools/scripts/local-bottom/meson.build @@ -4,9 +4,3 @@ configure_file( install_dir: join_paths(initramfs_scripts_dir, 'local-bottom'), configuration: initramfs_data, ) -configure_file( - input: 'clevis-zfs.in', - output: 'clevis-zfs', - install_dir: join_paths(initramfs_scripts_dir, 'local-bottom'), - configuration: initramfs_data, -) diff --git a/src/initramfs-tools/scripts/local-top/meson.build b/src/initramfs-tools/scripts/local-top/meson.build index d0f5df0f..38fca25a 100644 --- a/src/initramfs-tools/scripts/local-top/meson.build +++ b/src/initramfs-tools/scripts/local-top/meson.build @@ -4,9 +4,3 @@ configure_file( install_dir: join_paths(initramfs_scripts_dir, 'local-top'), configuration: initramfs_data, ) -configure_file( - input: 'clevis-zfs.in', - output: 'clevis-zfs', - install_dir: join_paths(initramfs_scripts_dir, 'local-top'), - configuration: initramfs_data, -) diff --git a/src/zfs/clevis-zfs-bind b/src/zfs/clevis-zfs-bind index 520aa56d..cea540ff 100755 --- a/src/zfs/clevis-zfs-bind +++ b/src/zfs/clevis-zfs-bind @@ -1,7 +1,6 @@ #!/bin/bash set -euo pipefail - SUMMARY="Binds a ZFS dataset using the specified policy" function usage() { @@ -11,17 +10,16 @@ function usage() { $SUMMARY: -f Do not prompt when overwriting configuration + -d DATASET The ZFS dataset on which to perform binding + -l LABEL The label to use for this binding. Valid characters: letters, numbers and underscores - -d DATASET The zfs dataset on which to perform binding - -l LABEL The label to use for this binding, can use letters, numbers and underscores - - -k KEY Non-interactively read zfs password from KEY file - -k - Non-interactively read zfs password from standard input + -k KEY Non-interactively read ZFS password from KEY file + -k - Non-interactively read ZFS password from standard input USAGE_END } -findexe() { +function findexe() { while read -r -d: path; do [ -f "${path}/${1}" ] && [ -x "${path}/${1}" ] && \ echo "${path}/${1}" && return 0 @@ -29,19 +27,18 @@ findexe() { return 1 } - function bind_zfs_dataset() { - local dataset="${1}" + local dataset="${1}" local label="${2}" - local pin="${3}" - local cfg="${4}" - local key="${5}" - local overwrite="${6:-}" + local pin="${3}" + local cfg="${4}" + local key="${5}" + local overwrite="${6:-}" local existing_key clevis_data if [[ -z "${overwrite}" ]] && zfs_is_bound "${dataset}" "${label}"; then - error "given label ${label} in dataset ${dataset} already has a clevis binding, not overwriting." + error "given label ${label} in dataset ${dataset} already has a Clevis binding, not overwriting." fi existing_key="$(read_passphrase "${dataset}" "${key}")" @@ -50,8 +47,8 @@ function bind_zfs_dataset() { error "given key does not unlock ${dataset}" fi - echo >&2 -n 'creating new clevis data... ' - clevis_data="$(clevis encrypt "${pin}" "${cfg}" <<<"${existing_key}" )" + echo >&2 -n 'creating new Clevis data... ' + clevis_data="$(clevis encrypt "${pin}" "${cfg}" <<<"${existing_key}")" echo >&2 'ok' [[ -n "${overwrite}" ]] && zfs_unbind_clevis_label "${dataset}" "${label}" && echo >&2 'unbound old clevis data' @@ -60,7 +57,6 @@ function bind_zfs_dataset() { } function main() { - if [ $# -eq 1 ] && [ "${1:-}" == "--summary" ]; then echo "$SUMMARY" exit 0 @@ -74,7 +70,8 @@ function main() { local overwrite='' while getopts ":hfd:l:k:" o; do case "$o" in - f) overwrite='yes' ;; + h) usage; exit 0;; + f) overwrite='yes';; d) dataset="$OPTARG";; l) label="$OPTARG";; k) key="$OPTARG";; @@ -83,24 +80,25 @@ function main() { done if [ -z "${dataset:-""}" ]; then - error "did not specify a device!" + error "did not specify a dataset!" fi check_valid_dataset "${dataset}" if [ -z "${label:-}" ]; then - error "Did not specify a label!" + error "did not specify a label!" fi - - if ! pin=${@:$((OPTIND++)):1} || [ -z "$pin" ]; then - error "Did not specify a pin!" + pin="${*:$((OPTIND++)):1}" + if [ -z "$pin" ]; then + error "did not specify a pin!" elif ! findexe clevis-encrypt-"${pin}" &>/dev/null; then error "'$pin' is not a valid pin!" fi - if ! cfg=${@:$((OPTIND++)):1} || [ -z "$cfg" ]; then - error "Did not specify a pin config!" + cfg="${*:$((OPTIND++)):1}" + if [ -z "$cfg" ]; then + error "did not specify a pin config!" fi bind_zfs_dataset "${dataset}" "${label}" "${pin}" "${cfg}" "${key}" "${overwrite}" diff --git a/src/zfs/clevis-zfs-common b/src/zfs/clevis-zfs-common index 5db5a2bc..12e3203d 100755 --- a/src/zfs/clevis-zfs-common +++ b/src/zfs/clevis-zfs-common @@ -42,7 +42,7 @@ function zfs_set_property() { } # defaults to getting just the value of the given property and only when it is set directly on the dataset ("local") -zfs_get_property() { +function zfs_get_property() { local dataset="${1}" local property="${2}" shift 2 @@ -52,7 +52,7 @@ zfs_get_property() { function zfs_load_key() { local dataset="${1}" local dry_run="${2:+-n}" - zfs load-key ${dry_run} -L prompt "${dataset}" >/dev/null + zfs load-key "${dry_run}" -L prompt "${dataset}" >/dev/null } function zfs_test_key() { @@ -64,7 +64,7 @@ function zfs_unload_key() { zfs unload-key "${dataset}" >/dev/null } -# ZFS properties functions to deal with clevis labels +# ZFS properties functions to deal with Clevis labels ############################################## # valid characters of clevis-zfs labels are: [0-9a-z_] @@ -102,7 +102,6 @@ function zfs_get_labels() { function zfs_get_label() { local dataset="${1}" local label="${2%%:*}" - zfs_get_labels "${dataset}" >&2 zfs_get_labels "${dataset}" | tr ' ' '\n' | grep -E "^${label}(:|$)" } @@ -117,8 +116,9 @@ function zfs_set_labels() { function zfs_add_label() { local dataset="${1}" local new_label="${2}" - local labels=( $(zfs_get_labels "${dataset}") ) - local labels+=( "${new_label}" ) + local labels + read -ra labels <<< "$(zfs_get_labels "${dataset}")" + labels+=( "${new_label}" ) zfs_set_labels "${dataset}" "${labels[*]}" } @@ -126,7 +126,8 @@ function zfs_add_label() { function zfs_remove_label() { local dataset="${1}" local old_label="${2}" - local labels=( $(zfs_get_labels "${dataset}") ) + local labels + read -ra labels <<< "$(zfs_get_labels "${dataset}")" local new_labels=( "${labels[@]/${old_label}}" ) zfs_set_labels "${dataset}" "${new_labels[*]}" } @@ -157,6 +158,7 @@ function zfs_is_encryptionroot() { # does it even exist? function zfs_is_dataset() { + local dataset="${1}" zfs_get_property "${dataset}" 'name' -snone &>/dev/null } @@ -164,7 +166,7 @@ function zfs_is_dataset() { function check_valid_dataset() { local dataset="${1}" - if ! zfs_is_dataset; then + if ! zfs_is_dataset "${dataset}"; then error "${dataset} is not a zfs dataset!" fi @@ -187,7 +189,7 @@ function read_passphrase() { "") IFS= read -r -s -p "Enter existing ZFS password for ${dataset}: " existing_key; echo >&2 ;; - -) IFS= read -r -s -p "" existing_key ;; + -) IFS= read -r -s -p "" existing_key;; *) keyfile="${key}" if [ -r "${keyfile}" ]; then existing_key="$(< "${keyfile}")" @@ -206,7 +208,7 @@ function error() { } -# functions to deal with too large clevis data for a single ZFS property +# functions to deal with too large Clevis data for a single ZFS property ######################################################################## function cut_into_chunks() { @@ -220,12 +222,14 @@ function zero_pad() { function num_list() { local last_index="${1}" - zero_pad "${#last_index}" $(eval "echo {0..${last_index}}") + local indices=() + read -ra indices <<< "$(eval "echo {0..${last_index}}")" + zero_pad "${#last_index}" "${indices[@]}" } -# functions to add/remove a clevis binding +# functions to add/remove a Clevis binding ######################################### function zfs_bind_clevis_label() { local dataset="${1}" @@ -234,12 +238,13 @@ function zfs_bind_clevis_label() { local zfs_label_prop="${zfs_label_prefix}:${label}" - echo >&2 -n 'binding new clevis data... ' + echo >&2 -n 'binding new Clevis data... ' # use a single prop without number suffix if it will fit in one prop if [[ "${#clevis_data}" -lt "${zfs_userprop_value_limit}" ]]; then zfs_set_property "${dataset}" "${zfs_label_prop}" "${clevis_data}" else - clevis_chunks=( $(cut_into_chunks <<<"${clevis_data}") ) + local clevis_chunks=() + read -ra clevis_chunks <<< "$(cut_into_chunks <<<"${clevis_data}")" last_index="$(( "${#clevis_chunks[@]}" - 1 ))" width="${#last_index}" @@ -259,7 +264,7 @@ function zfs_bind_clevis_label() { echo >&2 'ok' # check if unlocking works - echo >&2 -n 'testing new clevis data... ' + echo >&2 -n 'testing new Clevis data... ' # somehow clevis-decrypt exits with a non-zero code, but still outputs the # correct data, so we ignore the exit code. zfs_test_key will fail anyway @@ -304,9 +309,9 @@ function zfs_get_clevis_label() { if [[ "${label}" == "${last_index}" ]]; then zfs_get_property "${dataset}" "${zfs_label_prop}" else - clevis_data=() + local clevis_data=() for num in $(num_list "${last_index}"); do - clevis_data+=( $(zfs_get_property "${dataset}" "${zfs_label_prop}-${num}") ) + clevis_data+=( "$(zfs_get_property "${dataset}" "${zfs_label_prop}-${num}")" ) done local IFS='' echo "${clevis_data[*]}" @@ -314,7 +319,7 @@ function zfs_get_clevis_label() { } -function unlock_with_label(){ +function unlock_with_label() { local dataset="${1}" local label="${2}" local test_only="${3:-}" @@ -330,5 +335,5 @@ function unlock_with_label(){ if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then . clevis-zfs-test - _test "$@" + _test "$@" fi diff --git a/src/zfs/clevis-zfs-list b/src/zfs/clevis-zfs-list index 9777b6f8..038a5001 100755 --- a/src/zfs/clevis-zfs-list +++ b/src/zfs/clevis-zfs-list @@ -1,9 +1,7 @@ #!/bin/bash set -euo pipefail -. clevis-zfs-common - -SUMMARY="List zfs datasets that are bound with clevis [in dataset]" +SUMMARY="List ZFS datasets that are bound with Clevis [in dataset]" function usage() { cat >&2 <<-USAGE_END @@ -11,10 +9,11 @@ function usage() { $SUMMARY: + -d DATASET The ZFS dataset on which to perform unbinding + USAGE_END } - main() { if [ $# -eq 1 ] && [ "${1:-}" == "--summary" ]; then echo "$SUMMARY" @@ -22,23 +21,26 @@ main() { fi local dataset - while getopts "d:" o; do + while getopts "hd:" o; do case "$o" in - d) dataset="$OPTARG" ;; - *) error "unrecognized argument: -${OPTARG}" ;; + h) usage; exit 0;; + d) dataset="$OPTARG";; + *) error "unrecognized argument: -${OPTARG}";; esac done - if [ -n "${dataset}" ]; then + local output='name,value' + if [ -n "${dataset:-}" ]; then output='value' - else - output='name,value' fi - echo >&2 "The following ZFS datasets have been bound with clevis:" + echo >&2 "The following ZFS datasets have been bound with Clevis:" # we should not quote ${dataset:-} in case it is empty # shellcheck disable=SC2086 zfs get -H -o "${output}" -slocal "${zfs_labels_prop}" ${dataset:-} } -main "${@}" +if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then + . clevis-zfs-common + main "${@}" +fi diff --git a/src/zfs/clevis-zfs-test b/src/zfs/clevis-zfs-test index 096419fe..4f1e262a 100755 --- a/src/zfs/clevis-zfs-test +++ b/src/zfs/clevis-zfs-test @@ -85,8 +85,6 @@ function _test() { exit "${exit_code}" } - - function _test_is_valid_label() { local expected local result @@ -139,7 +137,6 @@ function _test_is_valid_label() { success } - function _test_read_passphrase() { # test with reading from stdin local expected @@ -162,18 +159,19 @@ function _test_read_passphrase() { [[ "${expected}" == "${result}" ]] && success || failed } - function _test_zero_pad() { testing 'pad with 4 zeroes' local expected='000051 ' - local result="$(zero_pad 6 51)" + local result + result="$(zero_pad 6 51)" [[ "${expected}" == "${result}" ]] && success || failed } function _test_num_list() { testing 'list with zero padding' local expected='00 01 02 03 04 05 06 07 08 09 10 ' - local result="$(num_list 10)" + local result + result="$(num_list 10)" [[ "${expected}" == "${result}" ]] && success || failed } diff --git a/src/zfs/clevis-zfs-unbind b/src/zfs/clevis-zfs-unbind index bbc110e2..c222d1f9 100755 --- a/src/zfs/clevis-zfs-unbind +++ b/src/zfs/clevis-zfs-unbind @@ -1,22 +1,21 @@ #!/bin/bash set -euo pipefail -. clevis-zfs-common - SUMMARY="Unbinds a label from a ZFS dataset" - function usage() { cat >&2 <<-USAGE_END - Usage: clevis zfs unbind [-k KEY] -d DATASET -l LABEL + Usage: clevis zfs unbind [-f] [-k KEY] -d DATASET [-a] -l LABEL $SUMMARY: - -d DATASET The zfs dataset on which to perform unbinding + -f Force unbinding dataset + -d DATASET The ZFS dataset on which to perform unbinding + -a Unbind all labels -l LABEL The label to unbind - -k KEY Non-interactively read zfs password from KEY file - -k - Non-interactively read zfs password from standard input + -k KEY Non-interactively read ZFS password from KEY file + -k - Non-interactively read ZFS password from standard input USAGE_END } @@ -46,13 +45,12 @@ function main() { done if [ -z "${dataset:-""}" ]; then - error "did not specify a device!" + error "did not specify a dataset!" fi if [[ "${force_unbind}" != 'true' ]]; then - if ! zfs_is_bound "${dataset}"; then - error "dataset is not bound with clevis: ${dataset}" + error "dataset is not bound with Clevis: ${dataset}" fi local existing_key @@ -64,19 +62,21 @@ function main() { fi fi - - - echo >&2 -n 'wiping clevis data... ' + echo >&2 -n 'Wiping Clevis data... ' if [[ "${unbind_all}" == 'true' ]]; then - labels="$(zfs_get_labels "${dataset}")" - for label in ${labels}; do + local labels=() + read -ra labels <<< "$(zfs_get_labels "${dataset}")" + for label in "${labels[@]}"; do zfs_unbind_clevis_label "${dataset}" "${label}" done else zfs_unbind_clevis_label "${dataset}" "${label}" fi - echo >&2 'ok' + echo >&2 'ok' } -main "${@}" +if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then + . clevis-zfs-common + main "${@}" +fi diff --git a/src/zfs/clevis-zfs-unlock b/src/zfs/clevis-zfs-unlock index 03149df3..0b7e78fe 100755 --- a/src/zfs/clevis-zfs-unlock +++ b/src/zfs/clevis-zfs-unlock @@ -1,23 +1,21 @@ #!/bin/bash set -euo pipefail -SUMMARY="Unlock a ZFS dataset using the saved clevis data" +SUMMARY="Unlock a ZFS dataset using the saved Clevis data" function usage() { cat >&2 <<-USAGE_END - Usage: clevis zfs unlock [-n] [-k KEY] -d DATASET PIN CFG + Usage: clevis zfs unlock [-t] [-l LABEL] -d DATASET $SUMMARY: - -t Test the clevis configuration without unlocking - -d DATASET The zfs dataset to unlock + -t Test the Clevis configuration without unlocking + -d DATASET The ZFS dataset to unlock -l LABEL Use only this label to unlock (defaults to trying all labels) USAGE_END } - - function main() { if [ $# -eq 1 ] && [ "${1:-}" == "--summary" ]; then echo "$SUMMARY" @@ -27,25 +25,26 @@ function main() { local dataset local test_only='' local label='' - while getopts ":d:l:t" o; do + while getopts "h:d:l:t" o; do case "$o" in - d) dataset="$OPTARG" ;; - t) test_only=' (test)' ;; - l) label="$OPTARG" ;; - *) error "unrecognized argument: -${OPTARG}" ;; + h) usage; exit 0;; + d) dataset="$OPTARG";; + t) test_only=' (test)';; + l) label="$OPTARG";; + *) error "unrecognized argument: -${OPTARG}";; esac done if [ -z "${dataset:-""}" ]; then - error "did not specify a device!" + error "did not specify a dataset!" fi if ! zfs_is_bound "${dataset}"; then - error "dataset is not bound with clevis: ${dataset}" + error "dataset is not bound with Clevis: ${dataset}" fi if [[ -n "${label}" ]]; then - label="$(zfs_get_label "${dataset}" "${label}" )" + label="$(zfs_get_label "${dataset}" "${label}")" testing -n "unlocking ${dataset} with ${label} ${test_only}... " if unlock_with_label "${dataset}" "${label}" "${test_only}"; then testing 'ok' From c3cfa331e0dfd1635a598bfe0f5b4847754c1282 Mon Sep 17 00:00:00 2001 From: Joel Low Date: Wed, 12 Jun 2024 10:58:47 +0800 Subject: [PATCH 12/20] Fix zfs-load-key dry-run detection --- src/zfs/clevis-zfs-common | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/zfs/clevis-zfs-common b/src/zfs/clevis-zfs-common index 12e3203d..79855b6e 100755 --- a/src/zfs/clevis-zfs-common +++ b/src/zfs/clevis-zfs-common @@ -51,8 +51,11 @@ function zfs_get_property() { function zfs_load_key() { local dataset="${1}" - local dry_run="${2:+-n}" - zfs load-key "${dry_run}" -L prompt "${dataset}" >/dev/null + local args=( "-L" "prompt" ) + if [[ -n "${2:-}" ]]; then + args+=( "-n" ) + fi + zfs load-key "${args[@]}" "${dataset}" >/dev/null } function zfs_test_key() { From 2828cabcb3c69ad5144201ad333eeec66b358f73 Mon Sep 17 00:00:00 2001 From: Joel Low Date: Wed, 12 Jun 2024 11:12:53 +0800 Subject: [PATCH 13/20] Print error messages before the help message --- src/zfs/clevis-zfs-common | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/zfs/clevis-zfs-common b/src/zfs/clevis-zfs-common index 79855b6e..7239353e 100755 --- a/src/zfs/clevis-zfs-common +++ b/src/zfs/clevis-zfs-common @@ -205,8 +205,8 @@ function read_passphrase() { } function error() { - usage echo >&2 -e "ERROR: ${*}" + usage exit 1 } From 79596e0159378c196986d535934911b3612c68b1 Mon Sep 17 00:00:00 2001 From: Joel Low Date: Wed, 12 Jun 2024 10:58:14 +0800 Subject: [PATCH 14/20] Normalise all instances of `findexe` --- src/zfs/clevis-zfs-bind | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/zfs/clevis-zfs-bind b/src/zfs/clevis-zfs-bind index cea540ff..10d65c9e 100755 --- a/src/zfs/clevis-zfs-bind +++ b/src/zfs/clevis-zfs-bind @@ -20,11 +20,11 @@ function usage() { } function findexe() { - while read -r -d: path; do - [ -f "${path}/${1}" ] && [ -x "${path}/${1}" ] && \ - echo "${path}/${1}" && return 0 - done <<< "${PATH}:" - return 1 + [ $# -eq 1 ] || return 1 + while read -r -d: path; do + [ -f "$path/$1" ] && [ -x "$path/$1" ] && echo "$path/$1" && return 0 + done <<< "$PATH:" + return 1 } function bind_zfs_dataset() { @@ -92,7 +92,7 @@ function main() { pin="${*:$((OPTIND++)):1}" if [ -z "$pin" ]; then error "did not specify a pin!" - elif ! findexe clevis-encrypt-"${pin}" &>/dev/null; then + elif ! eval "findexe clevis-encrypt-${pin}" &>/dev/null; then error "'$pin' is not a valid pin!" fi From a3cbbf22c84617528b432acc34b6816b1b077a6f Mon Sep 17 00:00:00 2001 From: Joel Low Date: Wed, 12 Jun 2024 11:38:31 +0800 Subject: [PATCH 15/20] Remove the ZFS property when last label is unbound --- src/zfs/clevis-zfs-common | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/zfs/clevis-zfs-common b/src/zfs/clevis-zfs-common index 7239353e..c77fabcf 100755 --- a/src/zfs/clevis-zfs-common +++ b/src/zfs/clevis-zfs-common @@ -132,7 +132,11 @@ function zfs_remove_label() { local labels read -ra labels <<< "$(zfs_get_labels "${dataset}")" local new_labels=( "${labels[@]/${old_label}}" ) - zfs_set_labels "${dataset}" "${new_labels[*]}" + if [[ "${#new_labels}" -eq 0 ]]; then + zfs_remove_property "${dataset}" "${zfs_labels_prop}" + else + zfs_set_labels "${dataset}" "${new_labels[*]}" + fi } # functions for checking zfs datasets From 2a01ca887db3ad8ca0c3372cc3321f0a523ccd3d Mon Sep 17 00:00:00 2001 From: Joel Low Date: Wed, 12 Jun 2024 11:40:37 +0800 Subject: [PATCH 16/20] Check when no label is specified to be unbound --- src/zfs/clevis-zfs-unbind | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/zfs/clevis-zfs-unbind b/src/zfs/clevis-zfs-unbind index c222d1f9..f21351c1 100755 --- a/src/zfs/clevis-zfs-unbind +++ b/src/zfs/clevis-zfs-unbind @@ -48,6 +48,10 @@ function main() { error "did not specify a dataset!" fi + if [ -z "${label}" ] && [ "${unbind_all}" == 'false' ]; then + error "did not specify a label!" + fi + if [[ "${force_unbind}" != 'true' ]]; then if ! zfs_is_bound "${dataset}"; then error "dataset is not bound with Clevis: ${dataset}" From 8e851cd1e87c698542292e994266f70b686967c8 Mon Sep 17 00:00:00 2001 From: Joel Low Date: Tue, 11 Jun 2024 16:30:27 +0800 Subject: [PATCH 17/20] Add missing build scripts for ZFS integration --- src/meson.build | 1 + src/zfs/meson.build | 6 ++++++ 2 files changed, 7 insertions(+) create mode 100644 src/zfs/meson.build diff --git a/src/meson.build b/src/meson.build index c4e696f6..a8011788 100644 --- a/src/meson.build +++ b/src/meson.build @@ -2,6 +2,7 @@ subdir('bash') subdir('luks') subdir('pins') subdir('initramfs-tools') +subdir('zfs') bins += join_paths(meson.current_source_dir(), 'clevis-decrypt') mans += join_paths(meson.current_source_dir(), 'clevis-decrypt.1') diff --git a/src/zfs/meson.build b/src/zfs/meson.build new file mode 100644 index 00000000..2593be9e --- /dev/null +++ b/src/zfs/meson.build @@ -0,0 +1,6 @@ +bins += join_paths(meson.current_source_dir(), 'clevis-zfs-bind') +bins += join_paths(meson.current_source_dir(), 'clevis-zfs-common') +bins += join_paths(meson.current_source_dir(), 'clevis-zfs-list') +bins += join_paths(meson.current_source_dir(), 'clevis-zfs-test') +bins += join_paths(meson.current_source_dir(), 'clevis-zfs-unbind') +bins += join_paths(meson.current_source_dir(), 'clevis-zfs-unlock') From 10c470248107b9421bdf73e30d5156f687aa211f Mon Sep 17 00:00:00 2001 From: Joel Low Date: Tue, 11 Jun 2024 16:39:15 +0800 Subject: [PATCH 18/20] Implement loading the ZFS key at boot --- src/initramfs-tools/hooks/clevis.in | 1 + src/initramfs-tools/meson.build | 1 + src/initramfs-tools/scripts/meson.build | 1 + .../scripts/zfs-load-key/clevis-zfs.in | 22 +++++++++++++++++++ .../scripts/zfs-load-key/meson.build | 6 +++++ 5 files changed, 31 insertions(+) create mode 100644 src/initramfs-tools/scripts/zfs-load-key/clevis-zfs.in create mode 100644 src/initramfs-tools/scripts/zfs-load-key/meson.build diff --git a/src/initramfs-tools/hooks/clevis.in b/src/initramfs-tools/hooks/clevis.in index 1b406fdc..564fdbd4 100755 --- a/src/initramfs-tools/hooks/clevis.in +++ b/src/initramfs-tools/hooks/clevis.in @@ -62,6 +62,7 @@ copy_exec @bindir@/clevis-decrypt-null || die 1 "@bindir@/clevis-decrypt-null no copy_exec @bindir@/clevis-decrypt || die 1 "@bindir@/clevis-decrypt not found" copy_exec @bindir@/clevis-luks-common-functions || die 1 "@bindir@/clevis-luks-common-functions not found" copy_exec @bindir@/clevis-zfs-common || die 1 "@bindir@/clevis-zfs-common not found" +copy_exec @bindir@/clevis-zfs-unlock || die 1 "@bindir@/clevis-zfs-unlock not found" copy_exec @bindir@/clevis-luks-list || die 1 "@bindir@/clevis-luks-list not found" if [ -x @bindir@/clevis-decrypt-tpm2 ]; then copy_exec @bindir@/clevis-decrypt-tpm2 || die 1 "@bindir@/clevis-decrypt-tpm2 not found" diff --git a/src/initramfs-tools/meson.build b/src/initramfs-tools/meson.build index a4661c90..ce02209f 100644 --- a/src/initramfs-tools/meson.build +++ b/src/initramfs-tools/meson.build @@ -4,6 +4,7 @@ if initramfs_tools.found() initramfstools_dir = '/usr/share/initramfs-tools' initramfs_hooks_dir = '/usr/share/initramfs-tools/hooks' initramfs_scripts_dir = '/usr/share/initramfs-tools/scripts' + zfs_initramfs_load_key_scripts_dir = '/etc/zfs/initramfs-tools-load-key.d' initramfs_data = configuration_data() initramfs_data.merge_from(data) initramfs_data.set('initramfstoolsdir', initramfstools_dir) diff --git a/src/initramfs-tools/scripts/meson.build b/src/initramfs-tools/scripts/meson.build index 2e7678b2..7d9fecd2 100644 --- a/src/initramfs-tools/scripts/meson.build +++ b/src/initramfs-tools/scripts/meson.build @@ -1,2 +1,3 @@ subdir('local-top') subdir('local-bottom') +subdir('zfs-load-key') diff --git a/src/initramfs-tools/scripts/zfs-load-key/clevis-zfs.in b/src/initramfs-tools/scripts/zfs-load-key/clevis-zfs.in new file mode 100644 index 00000000..168229b7 --- /dev/null +++ b/src/initramfs-tools/scripts/zfs-load-key/clevis-zfs.in @@ -0,0 +1,22 @@ +#!/bin/bash +# +# Copyright (c) 2024 Joel Low +# +# Author: Joel Low +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# +set -eu + +clevis zfs unlock -d "${ENCRYPTIONROOT}" diff --git a/src/initramfs-tools/scripts/zfs-load-key/meson.build b/src/initramfs-tools/scripts/zfs-load-key/meson.build new file mode 100644 index 00000000..e730df65 --- /dev/null +++ b/src/initramfs-tools/scripts/zfs-load-key/meson.build @@ -0,0 +1,6 @@ +configure_file( + input: 'clevis-zfs.in', + output: 'clevis-zfs', + install_dir: zfs_initramfs_load_key_scripts_dir, + configuration: initramfs_data, +) From f07f3555f741cbd4696ab0bafecc66723adb972a Mon Sep 17 00:00:00 2001 From: Joel Low Date: Thu, 13 Jun 2024 18:42:03 +0800 Subject: [PATCH 19/20] Split the ZFS initramfs hook from the LUKS hook Installing the ZFS integration should not imply installing the LUKS integration. --- src/initramfs-tools/hooks/clevis-zfs.in | 96 +++++++++++++++++++++++++ src/initramfs-tools/hooks/clevis.in | 2 - src/initramfs-tools/hooks/meson.build | 8 ++- 3 files changed, 103 insertions(+), 3 deletions(-) create mode 100755 src/initramfs-tools/hooks/clevis-zfs.in diff --git a/src/initramfs-tools/hooks/clevis-zfs.in b/src/initramfs-tools/hooks/clevis-zfs.in new file mode 100755 index 00000000..110e5f99 --- /dev/null +++ b/src/initramfs-tools/hooks/clevis-zfs.in @@ -0,0 +1,96 @@ +#!/bin/bash +# +# Copyright (c) 2017 Shawn Rose +# Copyright (c) 2024 Joel Low +# Author: Shawn Rose +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# + + +PREREQ="" +prereqs() +{ + echo "$PREREQ" +} + +case $1 in +prereqs) + prereqs + exit 0 + ;; +esac + +. @initramfstoolsdir@/hook-functions + +die() { + code="$1" + msg="$2" + echo " (ERROR): $msg" >&2 + exit $1 +} + +find_binary() { + bin_name="$1" + resolved=$(command -v ${bin_name}) + [ -z "$resolved" ] && die 1 "Unable to find ${bin_name}" + echo "$resolved" +} + + +copy_exec @bindir@/clevis-decrypt-tang || die 1 "@bindir@/clevis-decrypt-tang not found" +copy_exec @bindir@/clevis-decrypt-sss || die 1 "@bindir@/clevis-decrypt-sss not found" +copy_exec @bindir@/clevis-decrypt-null || die 1 "@bindir@/clevis-decrypt-null not found" +copy_exec @bindir@/clevis-decrypt || die 1 "@bindir@/clevis-decrypt not found" +copy_exec @bindir@/clevis-zfs-common || die 1 "@bindir@/clevis-zfs-common not found" +copy_exec @bindir@/clevis-zfs-unlock || die 1 "@bindir@/clevis-zfs-unlock not found" +if [ -x @bindir@/clevis-decrypt-tpm2 ]; then + copy_exec @bindir@/clevis-decrypt-tpm2 || die 1 "@bindir@/clevis-decrypt-tpm2 not found" + tpm2_creatprimary_bin=$(find_binary "tpm2_createprimary") + tpm2_unseal_bin=$(find_binary "tpm2_unseal") + tpm2_load_bin=$(find_binary "tpm2_load") + tpm2_flushcontext=$(find_binary "tpm2_flushcontext") + copy_exec "${tpm2_creatprimary_bin}" || die 1 "Unable to copy ${tpm2_creatprimary_bin}" + copy_exec "${tpm2_unseal_bin}" || die 1 "Unable to copy ${tpm2_unseal_bin}" + copy_exec "${tpm2_load_bin}" || die 1 "Unable to copy ${tpm2_load_bin}" + copy_exec "${tpm2_flushcontext}" || die 1 "Unable to copy ${tpm2_flushcontext}" + for _LIBRARY in @libdir@/libtss2-tcti-device.so*; do + if [ -e "${_LIBRARY}" ]; then + copy_exec "${_LIBRARY}" || die 2 "Unable to copy ${_LIBRARY}" + fi + done + manual_add_modules tpm_crb + manual_add_modules tpm_tis +fi + + +jose_bin=$(find_binary "jose") +copy_exec "${jose_bin}" || die 2 "Unable to copy ${jose_bin}" + + +copy_exec @bindir@/clevis || die 1 "@bindir@/clevis not found" +curl_bin=$(find_binary "curl") +awk_bin=$(find_binary "awk") +bash_bin=$(find_binary "bash") +copy_exec "${curl_bin}" || die 2 "Unable to copy ${curl_bin} to initrd image" +copy_exec "${awk_bin}" || die 2 "Unable to copy ${awk_bin} to initrd image" +copy_exec "${bash_bin}" || die 2 "Unable to copy ${bash_bin} to initrd image" + +# Copy latest versions of shared objects needed for DNS resolution +for so in $(ldconfig -p | sed -nr 's/^\s*libnss_files\.so\.[0-9]+\s.*=>\s*//p'); do + copy_exec "${so}" +done +for so in $(ldconfig -p | sed -nr 's/^\s*libnss_dns\.so\.[0-9]+\s.*=>\s*//p'); do + copy_exec "${so}" +done diff --git a/src/initramfs-tools/hooks/clevis.in b/src/initramfs-tools/hooks/clevis.in index 564fdbd4..3d4eb67f 100755 --- a/src/initramfs-tools/hooks/clevis.in +++ b/src/initramfs-tools/hooks/clevis.in @@ -61,8 +61,6 @@ copy_exec @bindir@/clevis-decrypt-sss || die 1 "@bindir@/clevis-decrypt-sss not copy_exec @bindir@/clevis-decrypt-null || die 1 "@bindir@/clevis-decrypt-null not found" copy_exec @bindir@/clevis-decrypt || die 1 "@bindir@/clevis-decrypt not found" copy_exec @bindir@/clevis-luks-common-functions || die 1 "@bindir@/clevis-luks-common-functions not found" -copy_exec @bindir@/clevis-zfs-common || die 1 "@bindir@/clevis-zfs-common not found" -copy_exec @bindir@/clevis-zfs-unlock || die 1 "@bindir@/clevis-zfs-unlock not found" copy_exec @bindir@/clevis-luks-list || die 1 "@bindir@/clevis-luks-list not found" if [ -x @bindir@/clevis-decrypt-tpm2 ]; then copy_exec @bindir@/clevis-decrypt-tpm2 || die 1 "@bindir@/clevis-decrypt-tpm2 not found" diff --git a/src/initramfs-tools/hooks/meson.build b/src/initramfs-tools/hooks/meson.build index 9ac06c77..4eb52d5f 100644 --- a/src/initramfs-tools/hooks/meson.build +++ b/src/initramfs-tools/hooks/meson.build @@ -1,6 +1,12 @@ configure_file( input: 'clevis.in', - output: 'clevis', + output: 'clevis-luks', + install_dir: initramfs_hooks_dir, + configuration: initramfs_data, +) +configure_file( + input: 'clevis-zfs.in', + output: 'clevis-zfs', install_dir: initramfs_hooks_dir, configuration: initramfs_data, ) From 4317bd74fa544edfcc04ad57e35d238de796d109 Mon Sep 17 00:00:00 2001 From: Joel Low Date: Fri, 14 Jun 2024 16:45:04 +0800 Subject: [PATCH 20/20] Move clevis-zfs-common to /usr/libexec --- src/initramfs-tools/hooks/clevis-zfs.in | 2 +- src/zfs/clevis-zfs-bind | 2 +- src/zfs/clevis-zfs-list | 2 +- src/zfs/clevis-zfs-unbind | 2 +- src/zfs/clevis-zfs-unlock | 2 +- src/zfs/meson.build | 27 +++++++++++++++++++------ 6 files changed, 26 insertions(+), 11 deletions(-) diff --git a/src/initramfs-tools/hooks/clevis-zfs.in b/src/initramfs-tools/hooks/clevis-zfs.in index 110e5f99..9327cee5 100755 --- a/src/initramfs-tools/hooks/clevis-zfs.in +++ b/src/initramfs-tools/hooks/clevis-zfs.in @@ -53,7 +53,7 @@ copy_exec @bindir@/clevis-decrypt-tang || die 1 "@bindir@/clevis-decrypt-tang no copy_exec @bindir@/clevis-decrypt-sss || die 1 "@bindir@/clevis-decrypt-sss not found" copy_exec @bindir@/clevis-decrypt-null || die 1 "@bindir@/clevis-decrypt-null not found" copy_exec @bindir@/clevis-decrypt || die 1 "@bindir@/clevis-decrypt not found" -copy_exec @bindir@/clevis-zfs-common || die 1 "@bindir@/clevis-zfs-common not found" +copy_exec @libexecdir@/clevis-zfs-common || die 1 "@libexecdir@/clevis-zfs-common not found" copy_exec @bindir@/clevis-zfs-unlock || die 1 "@bindir@/clevis-zfs-unlock not found" if [ -x @bindir@/clevis-decrypt-tpm2 ]; then copy_exec @bindir@/clevis-decrypt-tpm2 || die 1 "@bindir@/clevis-decrypt-tpm2 not found" diff --git a/src/zfs/clevis-zfs-bind b/src/zfs/clevis-zfs-bind index 10d65c9e..9e999478 100755 --- a/src/zfs/clevis-zfs-bind +++ b/src/zfs/clevis-zfs-bind @@ -106,6 +106,6 @@ function main() { } if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then - . clevis-zfs-common + . @libexecdir@/clevis-zfs-common main "${@}" fi diff --git a/src/zfs/clevis-zfs-list b/src/zfs/clevis-zfs-list index 038a5001..c7efb371 100755 --- a/src/zfs/clevis-zfs-list +++ b/src/zfs/clevis-zfs-list @@ -41,6 +41,6 @@ main() { } if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then - . clevis-zfs-common + . @libexecdir@/clevis-zfs-common main "${@}" fi diff --git a/src/zfs/clevis-zfs-unbind b/src/zfs/clevis-zfs-unbind index f21351c1..5a542b7a 100755 --- a/src/zfs/clevis-zfs-unbind +++ b/src/zfs/clevis-zfs-unbind @@ -81,6 +81,6 @@ function main() { } if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then - . clevis-zfs-common + . @libexecdir@/clevis-zfs-common main "${@}" fi diff --git a/src/zfs/clevis-zfs-unlock b/src/zfs/clevis-zfs-unlock index 0b7e78fe..bf978ac4 100755 --- a/src/zfs/clevis-zfs-unlock +++ b/src/zfs/clevis-zfs-unlock @@ -81,6 +81,6 @@ function testing() { } if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then - . clevis-zfs-common + . @libexecdir@/clevis-zfs-common main "${@}" fi diff --git a/src/zfs/meson.build b/src/zfs/meson.build index 2593be9e..0aa37a31 100644 --- a/src/zfs/meson.build +++ b/src/zfs/meson.build @@ -1,6 +1,21 @@ -bins += join_paths(meson.current_source_dir(), 'clevis-zfs-bind') -bins += join_paths(meson.current_source_dir(), 'clevis-zfs-common') -bins += join_paths(meson.current_source_dir(), 'clevis-zfs-list') -bins += join_paths(meson.current_source_dir(), 'clevis-zfs-test') -bins += join_paths(meson.current_source_dir(), 'clevis-zfs-unbind') -bins += join_paths(meson.current_source_dir(), 'clevis-zfs-unlock') +install_data( + [join_paths(meson.current_source_dir(), 'clevis-zfs-common')], + install_dir: libexecdir +) + +zfs_bins = [ + 'clevis-zfs-bind', + 'clevis-zfs-list', + 'clevis-zfs-unbind', + 'clevis-zfs-unlock', +] +foreach b : zfs_bins + configure_file( + input: b, + output: b, + install_dir: bindir, + configuration: data + ) +endforeach + +test('clevis-zfs-test', find_program(join_paths(meson.current_source_dir(), 'clevis-zfs-test')))