From 5b0319d4b43d37483a69e22d960c6b307d1a7444 Mon Sep 17 00:00:00 2001 From: Vince van Oosten Date: Tue, 24 May 2022 21:40:54 +0200 Subject: [PATCH 01/10] 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 6ec090b3ac368003065734620f58213c1ae02d11 Mon Sep 17 00:00:00 2001 From: Vince van Oosten Date: Tue, 24 May 2022 22:02:46 +0200 Subject: [PATCH 02/10] 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 d9b798ba4bcc7068053b47682abf1de21f004cd9 Mon Sep 17 00:00:00 2001 From: Vince van Oosten Date: Wed, 25 May 2022 21:48:09 +0200 Subject: [PATCH 03/10] 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 09cc10c941ac4d8715cfa4aa839a58a21f116edd Mon Sep 17 00:00:00 2001 From: Vince van Oosten Date: Wed, 25 May 2022 23:23:55 +0200 Subject: [PATCH 04/10] 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 fd1f355788c2bc9604f31950be1681b6cce4d1d1 Mon Sep 17 00:00:00 2001 From: Vince van Oosten Date: Thu, 26 May 2022 10:22:51 +0200 Subject: [PATCH 05/10] 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 f918dd6c59fded1842d04ecce43b126fd69766fa Mon Sep 17 00:00:00 2001 From: Vince van Oosten Date: Thu, 26 May 2022 14:40:42 +0200 Subject: [PATCH 06/10] 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 ad920f5484e2a8d2dc091aced1d95fc1b36f2ae9 Mon Sep 17 00:00:00 2001 From: Vince van Oosten Date: Thu, 26 May 2022 15:25:04 +0200 Subject: [PATCH 07/10] 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 dbc1c075a7dc34a5f7a25ad8177201c6e19e1ea9 Mon Sep 17 00:00:00 2001 From: Vince van Oosten Date: Sun, 4 Dec 2022 10:49:56 +0100 Subject: [PATCH 08/10] 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 2c8473c3..1d4c1ea2 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 94a5a4d26b20a3f1333ed9c32d62a8006ceaa328 Mon Sep 17 00:00:00 2001 From: Vince van Oosten Date: Sun, 4 Dec 2022 10:59:18 +0100 Subject: [PATCH 09/10] 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 f5d8def17fee4726418ff3e408a62efccb4ea4a5 Mon Sep 17 00:00:00 2001 From: Vince van Oosten Date: Sun, 4 Dec 2022 11:00:35 +0100 Subject: [PATCH 10/10] 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 "${@}"