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 +} 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 new file mode 100755 index 00000000..520aa56d --- /dev/null +++ b/src/zfs/clevis-zfs-bind @@ -0,0 +1,113 @@ +#!/bin/bash +set -euo pipefail + + +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 + -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 + + 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}" + 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}" "${label}"; then + error "given label ${label} in dataset ${dataset} already has a clevis binding, not overwriting." + fi + + existing_key="$(read_passphrase "${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_unbind_clevis_label "${dataset}" "${label}" && echo >&2 'unbound old clevis data' + + zfs_bind_clevis_label "${dataset}" "${label}" "${clevis_data}" +} + +function main() { + + if [ $# -eq 1 ] && [ "${1:-}" == "--summary" ]; then + echo "$SUMMARY" + exit 0 + fi + + 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 + done + + if [ -z "${dataset:-""}" ]; then + error "did not specify a device!" + fi + + 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 + 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}" "${label}" "${pin}" "${cfg}" "${key}" "${overwrite}" + echo >&2 "label ${label} on dataset ${dataset} is succesfully bound" +} + +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 new file mode 100755 index 00000000..5db5a2bc --- /dev/null +++ b/src/zfs/clevis-zfs-common @@ -0,0 +1,334 @@ +#!/bin/bash +set -euo pipefail + +# zfs user properties are limited to 8192 bytes +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) +zfs_userprop_prefix='latchset.clevis' + +# 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" + + +# 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_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_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}" + local labels=( $(zfs_get_labels "${dataset}") ) + local labels+=( "${new_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}" + local labels=( $(zfs_get_labels "${dataset}") ) + local new_labels=( "${labels[@]/${old_label}}" ) + zfs_set_labels "${dataset}" "${new_labels[*]}" +} + +# functions for checking zfs datasets +##################################### + +# check if a dataset is bound to a specific label (or any label) +function zfs_is_bound() { + local dataset="${1}" + local label="${2:-}" + + if [[ -z "${label}" ]] && [[ -n "$(zfs_get_labels "${dataset}")" ]]; then + return 0 + elif [[ -n "$(zfs_get_label "${dataset}" "${label}")" ]]; then + return 0 + else + 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 +} + + +# functions to deal with I/O to the user +function read_passphrase() { + 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 +} + + +# functions to deal with too large clevis data for a single ZFS property +######################################################################## + +function cut_into_chunks() { + fold -w "${zfs_userprop_value_limit}" +} + +function zero_pad() { + local width="${1}"; shift + printf "%0${width}d " "${@}" +} + +function num_list() { + local last_index="${1}" + zero_pad "${#last_index}" $(eval "echo {0..${last_index}}") +} + + + +# functions to add/remove a clevis binding +######################################### +function zfs_bind_clevis_label() { + local dataset="${1}" + local label="${2}" + local clevis_data="${3}" + + local zfs_label_prop="${zfs_label_prefix}:${label}" + + 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}") ) + last_index="$(( "${#clevis_chunks[@]}" - 1 ))" + width="${#last_index}" + + local chunk chunk_num + 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}]}" + # e.g. latchset.clevis.label:${label}-01=chunk_data + zfs_set_property "${dataset}" "${zfs_label_prop}-${chunk_num}" "${chunk}" + done + + label="${label}:${last_index}" + fi + echo >&2 'ok' + + # check if unlocking works + 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 + # 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_unbind_clevis_label() { + local dataset="${1}" + local label="${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_remove_property "${dataset}" "${zfs_label_prop}" + else + 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%:*}" + local last_index="${2#*:}" + + local zfs_label_prop="${zfs_label_prefix}:${label}" + if [[ "${label}" == "${last_index}" ]]; then + zfs_get_property "${dataset}" "${zfs_label_prop}" + else + 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 "$@" +fi diff --git a/src/zfs/clevis-zfs-list b/src/zfs/clevis-zfs-list new file mode 100755 index 00000000..9777b6f8 --- /dev/null +++ b/src/zfs/clevis-zfs-list @@ -0,0 +1,44 @@ +#!/bin/bash +set -euo pipefail + +. clevis-zfs-common + +SUMMARY="List zfs datasets that are bound with clevis [in dataset]" + +function usage() { + cat >&2 <<-USAGE_END + Usage: clevis zfs list -d DATASET + + $SUMMARY: + + USAGE_END +} + + +main() { + if [ $# -eq 1 ] && [ "${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 + + 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 ${dataset:-} in case it is empty + # shellcheck disable=SC2086 + zfs get -H -o "${output}" -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..096419fe --- /dev/null +++ b/src/zfs/clevis-zfs-test @@ -0,0 +1,244 @@ +#!/bin/bash +set -euo pipefail + +zpool='clevis-zfs-pool' +root_dataset="${zpool}/clevis" +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 twice (i.e. >16k) +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' + 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} + + 5) set tang_host and tang_thp to an existing tang server in ${BASH_SOURCE[0]} + 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_passphrase + _test_zero_pad + _test_num_list + _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}" &>${shutup}; 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}" &>${shutup}; then + failed "for \`is_valid_label '${l}'\`" + fi + done + success +} + + +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_passphrase "mydataset" '' <<<"${expected}" 2>/dev/null)" + [[ "${expected}" == "${result}" ]] && success || failed + + testing 'key test: dash argument (stdin)' + expected='dash-argument-password' + result="$(read_passphrase "mydataset" '-' <<<"${expected}" 2>/dev/null)" + [[ "${expected}" == "${result}" ]] && success || failed + + + testing 'key arg: filename' + expected='filename-argument-password' + 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 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}" &>${shutup} && success && return 0 + zfs destroy -f -r "${test_dataset}" + success +} + +function _zfs_create_encrypted_dataset() { + local dataset="${1}" + testing "creating encrypted test dataset: ${dataset}" + # 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 + if zfs list "${dataset}" &>${shutup}; then + success + return 0 + else + return 1 + fi +} + +function _test_zfs_functions() { + testing "checking if zfs testing dataset exists: ${root_dataset}" + 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 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 + # 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 + + expected='unlock test success' + result='unlock test failure' + testing 'testing unlocking with bindings tang' + ./clevis-zfs-unlock -t -d "${test_dataset}" -l 'tang_testlabel' &>${shutup} && success || failed + + 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' + 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 + + 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 new file mode 100755 index 00000000..bbc110e2 --- /dev/null +++ b/src/zfs/clevis-zfs-unbind @@ -0,0 +1,82 @@ +#!/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 + + $SUMMARY: + + -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 + + USAGE_END +} + + +function main() { + if [ $# -eq 1 ] && [ "${1:-}" == "--summary" ]; then + echo "$SUMMARY" + exit 0 + fi + + local dataset= + local key= + local label= + 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: -${o}";; + esac + done + + if [ -z "${dataset:-""}" ]; then + error "did not specify a device!" + 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}")" + + if ! zfs_test_key "${dataset}" <<<"${existing_key}"; then + error "given key does not unlock ${dataset}" + fi + fi + + + + echo >&2 -n 'wiping clevis data... ' + 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 "${@}" diff --git a/src/zfs/clevis-zfs-unlock b/src/zfs/clevis-zfs-unlock new file mode 100755 index 00000000..03149df3 --- /dev/null +++ b/src/zfs/clevis-zfs-unlock @@ -0,0 +1,87 @@ +#!/bin/bash +set -euo pipefail + +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 + -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" + exit 0 + fi + + local dataset + local test_only='' + local label='' + while getopts ":d:l:t" o; do + case "$o" in + 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!" + fi + + if ! zfs_is_bound "${dataset}"; then + error "dataset is not bound with clevis: ${dataset}" + fi + + 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 + testing 'failed' + exit 1 + fi + else + local labels + 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 "${@}" || true +} + +if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then + . clevis-zfs-common + main "${@}" +fi