From e5f6d87d5c71f3faf0c0dbe38534fd3eab30f43e Mon Sep 17 00:00:00 2001 From: Sergio Correia Date: Wed, 13 May 2020 23:51:04 -0300 Subject: [PATCH 2/8] Fix clevis luks unlock and add related tests --- src/luks/clevis-luks-common-functions | 35 ++++++ src/luks/clevis-luks-unlock | 68 ++++++++++++ src/luks/clevis-luks-unlock.in | 130 ---------------------- src/luks/meson.build | 10 +- src/luks/tests/meson.build | 40 +++++++ src/luks/tests/tests-common-functions.in | 134 +++++++++++++++++++++-- src/luks/tests/unlock-tang-luks1 | 83 ++++++++++++++ src/luks/tests/unlock-tang-luks2 | 83 ++++++++++++++ 8 files changed, 439 insertions(+), 144 deletions(-) create mode 100755 src/luks/clevis-luks-unlock delete mode 100755 src/luks/clevis-luks-unlock.in create mode 100755 src/luks/tests/unlock-tang-luks1 create mode 100755 src/luks/tests/unlock-tang-luks2 diff --git a/src/luks/clevis-luks-common-functions b/src/luks/clevis-luks-common-functions index e27c444..d04fdb5 100644 --- a/src/luks/clevis-luks-common-functions +++ b/src/luks/clevis-luks-common-functions @@ -281,3 +281,38 @@ clevis_luks_read_pins_from_slot() { fi printf "%s: %s\n" "${SLOT}" "${cfg}" } + +# clevis_luks_unlock_device() does the unlock of the device passed as +# parameter and returns the decoded passphrase. +clevis_luks_unlock_device() { + local DEV="${1}" + [ -z "${DEV}" ] && return 1 + + local used_slots + if ! used_slots=$(clevis_luks_used_slots "${DEV}") \ + || [ -z "${used_slots}" ]; then + return 1 + fi + + local slt jwe passphrase + for slt in ${used_slots}; do + if ! jwe="$(clevis_luks_read_slot "${DEV}" "${slt}" 2>/dev/null)" \ + || [ -z "${jwe}" ]; then + continue + fi + + if ! passphrase="$(clevis decrypt < <(echo -n "${jwe}"))" \ + || [ -z "${passphrase}" ]; then + continue + fi + + if ! cryptsetup luksOpen --test-passphrase "${DEV}" \ + --key-file <(echo -n "${passphrase}"); then + continue + fi + echo -n "${passphrase}" + return 0 + done + + return 1 +} diff --git a/src/luks/clevis-luks-unlock b/src/luks/clevis-luks-unlock new file mode 100755 index 0000000..580fde8 --- /dev/null +++ b/src/luks/clevis-luks-unlock @@ -0,0 +1,68 @@ +#!/bin/bash -e +# 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 . +# +. clevis-luks-common-functions + +SUMMARY="Unlocks a LUKS volume" + +function usage() { + exec >&2 + echo + echo "Usage: clevis luks unlock -d DEV [-n NAME]" + echo + echo "$SUMMARY": + echo + echo " -d DEV The LUKS device on which to perform unlocking" + echo + echo " -n NAME The name of the unlocked device node" + echo + exit 2 +} + +if [ $# -eq 1 ] && [ "$1" == "--summary" ]; then + echo "$SUMMARY" + exit 0 +fi + +while getopts ":d:n:" o; do + case "$o" in + d) DEV="$OPTARG";; + n) NAME="$OPTARG";; + *) usage;; + esac +done + +if [ -z "$DEV" ]; then + echo "Did not specify a device!" >&2 + usage +fi + +if ! cryptsetup isLuks "$DEV"; then + echo "$DEV is not a LUKS device!" >&2 + exit 1 +fi + +NAME="${NAME:-luks-"$(cryptsetup luksUUID "$DEV")"}" + +if ! pt=$(clevis_luks_unlock_device "${DEV}"); then + echo "${DEV} could not be opened." >&2 + exit 1 +fi + +cryptsetup open -d- "${DEV}" "${NAME}" < <(echo -n "${pt}") diff --git a/src/luks/clevis-luks-unlock.in b/src/luks/clevis-luks-unlock.in deleted file mode 100755 index aa3134b..0000000 --- a/src/luks/clevis-luks-unlock.in +++ /dev/null @@ -1,130 +0,0 @@ -#!/bin/bash -e -# 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 . -# - -SUMMARY="Unlocks a LUKS volume" -UUID=cb6e8904-81ff-40da-a84a-07ab9ab5715e - -# We require cryptsetup >= 2.0.4 to fully support LUKSv2. -# Support is determined at build time. -function luks2_supported() { - return @OLD_CRYPTSETUP@ -} - -function usage() { - exec >&2 - echo - echo "Usage: clevis luks unlock -d DEV [-n NAME]" - echo - echo "$SUMMARY": - echo - echo " -d DEV The LUKS device on which to perform unlocking" - echo - echo " -n NAME The name of the unlocked device node" - echo - exit 2 -} - -if [ $# -eq 1 ] && [ "$1" == "--summary" ]; then - echo "$SUMMARY" - exit 0 -fi - -while getopts ":d:n:" o; do - case "$o" in - d) DEV="$OPTARG";; - n) NAME="$OPTARG";; - *) usage;; - esac -done - -if [ -z "$DEV" ]; then - echo "Did not specify a device!" >&2 - usage -fi - -if ! cryptsetup isLuks "$DEV"; then - echo "$DEV is not a LUKS device!" >&2 - exit 1 -fi - -if luks2_supported; then - if cryptsetup isLuks --type luks1 "$DEV"; then - luks_type="luks1" - elif cryptsetup isLuks --type luks2 "$DEV";then - luks_type="luks2" - else - echo "$DEV is not a supported LUKS device!" >&2 - exit 1 - fi -else - luks_type="luks1" -fi -NAME="${NAME:-luks-"$(cryptsetup luksUUID "$DEV")"}" - -luks1_decrypt() { - luksmeta load "$@" \ - | clevis decrypt - - local rc - for rc in "${PIPESTATUS[@]}"; do - [ $rc -eq 0 ] || return $rc - done - return 0 -} - -luks2_decrypt() { - # jose jwe fmt -c outputs extra \n, so clean it up - cryptsetup token export "$@" \ - | jose fmt -j- -Og jwe -o- \ - | jose jwe fmt -i- -c \ - | tr -d '\n' \ - | clevis decrypt - - local rc - for rc in "${PIPESTATUS[@]}"; do - [ $rc -eq 0 ] || return $rc - done - return 0 -} - -if [ "$luks_type" == "luks1" ]; then - while read -r slot state uuid; do - [ "$state" == "active" ] || continue - [ "$uuid" == "$UUID" ] || continue - - pt="$(luks1_decrypt -d $DEV -s $slot -u $UUID)" \ - || continue - exec cryptsetup open -d- "$DEV" "$NAME" < <( - echo -n "$pt" - ) - done < <(luksmeta show -d "$DEV") - -elif [ "$luks_type" == "luks2" ]; then - while read -r id; do - pt="$(luks2_decrypt --token-id "$id" "$DEV")" \ - || continue - exec cryptsetup open -d- "$DEV" "$NAME" < <( - echo -n "$pt" - ) - done < <(cryptsetup luksDump "$DEV" | sed -rn 's|^\s+([0-9]+): clevis|\1|p') -fi - -echo "$DEV could not be opened." >&2 -exit 1 diff --git a/src/luks/meson.build b/src/luks/meson.build index bbba63f..0d24f8d 100644 --- a/src/luks/meson.build +++ b/src/luks/meson.build @@ -21,9 +21,7 @@ clevis_luks_bind = configure_file(input: 'clevis-luks-bind.in', clevis_luks_unbind = configure_file(input: 'clevis-luks-unbind.in', output: 'clevis-luks-unbind', configuration: luksmeta_data) -clevis_luks_unlock = configure_file(input: 'clevis-luks-unlock.in', - output: 'clevis-luks-unlock', - configuration: luksmeta_data) + if libcryptsetup.found() and luksmeta.found() and pwmake.found() subdir('systemd') subdir('udisks2') @@ -31,18 +29,18 @@ if libcryptsetup.found() and luksmeta.found() and pwmake.found() bins += clevis_luks_unbind mans += join_paths(meson.current_source_dir(), 'clevis-luks-unbind.1') - bins += clevis_luks_unlock - mans += join_paths(meson.current_source_dir(), 'clevis-luks-unlock.1') - bins += clevis_luks_bind mans += join_paths(meson.current_source_dir(), 'clevis-luks-bind.1') mans += join_paths(meson.current_source_dir(), 'clevis-luks-unlockers.7') bins += join_paths(meson.current_source_dir(), 'clevis-luks-common-functions') + bins += join_paths(meson.current_source_dir(), 'clevis-luks-list') mans += join_paths(meson.current_source_dir(), 'clevis-luks-list.1') + bins += join_paths(meson.current_source_dir(), 'clevis-luks-unlock') + mans += join_paths(meson.current_source_dir(), 'clevis-luks-unlock.1') else warning('Will not install LUKS support due to missing dependencies!') endif diff --git a/src/luks/tests/meson.build b/src/luks/tests/meson.build index 2e0fb92..9a16b42 100644 --- a/src/luks/tests/meson.build +++ b/src/luks/tests/meson.build @@ -1,6 +1,30 @@ # We use jq for comparing the pin config in the clevis luks list tests. jq = find_program('jq', required: false) +# we use systemd-socket-activate for running test tang servers. +actv = find_program( + 'systemd-socket-activate', + 'systemd-activate', + required: false +) + +kgen = find_program( + join_paths(libexecdir, 'tangd-keygen'), + join_paths(get_option('prefix'), get_option('libdir'), 'tangd-keygen'), + join_paths(get_option('prefix'), get_option('libexecdir'), 'tangd-keygen'), + join_paths('/', 'usr', get_option('libdir'), 'tangd-keygen'), + join_paths('/', 'usr', get_option('libexecdir'), 'tangd-keygen'), + required: false +) +tang = find_program( + join_paths(libexecdir, 'tangd'), + join_paths(get_option('prefix'), get_option('libdir'), 'tangd'), + join_paths(get_option('prefix'), get_option('libexecdir'), 'tangd'), + join_paths('/', 'usr', get_option('libdir'), 'tangd'), + join_paths('/', 'usr', get_option('libexecdir'), 'tangd'), + required: false +) + common_functions = configure_file(input: 'tests-common-functions.in', output: 'tests-common-functions', configuration: luksmeta_data, @@ -24,6 +48,14 @@ env.prepend('PATH', separator: ':' ) +has_tang = false +if actv.found() and kgen.found() and tang.found() + has_tang = true + env.set('SD_ACTIVATE', actv.path()) + env.set('TANGD_KEYGEN', kgen.path()) + env.set('TANGD', tang.path()) +endif + test('bind-wrong-pass-luks1', find_program('bind-wrong-pass-luks1'), env: env) test('bind-luks1', find_program('bind-luks1'), env: env) test('unbind-unbound-slot-luks1', find_program('unbind-unbound-slot-luks1'), env: env) @@ -42,6 +74,10 @@ else warning('Will not run "clevis luks list" tests due to missing jq dependency') endif +if has_tang + test('unlock-tang-luks1', find_program('unlock-tang-luks1'), env: env, timeout: 90) +endif + # LUKS2 tests go here, and they get included if we get support for it, based # on the cryptsetup version. # Binding LUKS2 takes longer, so timeout is increased for a few tests. @@ -56,4 +92,8 @@ if luksmeta_data.get('OLD_CRYPTSETUP') == '0' test('list-tang-luks2', find_program('list-tang-luks2'), env: env, timeout: 60) test('list-sss-tang-luks2', find_program('list-sss-tang-luks2'), env: env, timeout: 60) endif + + if has_tang + test('unlock-tang-luks2', find_program('unlock-tang-luks2'), env: env, timeout: 120) + endif endif diff --git a/src/luks/tests/tests-common-functions.in b/src/luks/tests/tests-common-functions.in index 90420d1..7b3fdad 100755 --- a/src/luks/tests/tests-common-functions.in +++ b/src/luks/tests/tests-common-functions.in @@ -56,7 +56,7 @@ new_device() { # Some builders fail if the cryptsetup steps are not ran as root, so let's # skip the test now if not running as root. - if [ $(id -u) != 0 ]; then + if [ "$(id -u)" != 0 ]; then skip_test "WARNING: You must be root to run this test; test skipped." fi @@ -74,9 +74,9 @@ new_device() { return 0 fi - fallocate -l16M "${DEV}" - local extra_options='--pbkdf pbkdf2 --pbkdf-force-iterations 1000' - cryptsetup luksFormat --type "${LUKS}" ${extra_options} --batch-mode \ + fallocate -l64M "${DEV}" + cryptsetup luksFormat --type "${LUKS}" --pbkdf pbkdf2 \ + --pbkdf-force-iterations 1000 --batch-mode \ --force-password "${DEV}" <<< "${PASS}" # Caching the just-formatted device for possible reuse. cp -f "${DEV}" "${DEV_CACHED}" @@ -90,7 +90,7 @@ new_device_keyfile() { # Some builders fail if the cryptsetup steps are not ran as root, so let's # skip the test now if not running as root. - if [ $(id -u) != 0 ]; then + if [ "$(id -u)" != 0 ]; then skip_test "WARNING: You must be root to run this test; test skipped." fi @@ -98,9 +98,9 @@ new_device_keyfile() { error "Invalid keyfile (${KEYFILE})." fi - fallocate -l16M "${DEV}" - local extra_options='--pbkdf pbkdf2 --pbkdf-force-iterations 1000' - cryptsetup luksFormat --type "${LUKS}" ${extra_options} --batch-mode \ + fallocate -l64M "${DEV}" + cryptsetup luksFormat --type "${LUKS}" --pbkdf pbkdf2 \ + --pbkdf-force-iterations 1000 --batch-mode \ "${DEV}" "${KEYFILE}" } @@ -112,4 +112,122 @@ pin_cfg_equal() { <(jq -S . < <(echo -n "${cfg2}")) } +# Get a random port to be used with a test tang server. +get_random_port() { + shuf -i 1024-65535 -n 1 +} + +# Removes tang rotated keys from the test server. +tang_remove_rotated_keys() { + local basedir="${1}" + + if [ -z "${basedir}" ]; then + echo "Please pass a valid base directory for tang" + return 1 + fi + + local db="${basedir}/db" + mkdir -p "${db}" + + pushd "${db}" + find . -name ".*.jwk" -exec rm -f {} \; + popd +} + +# Creates new keys for the test tang server. +tang_new_keys() { + local basedir="${1}" + local rotate="${2}" + + if [ -z "${basedir}" ]; then + echo "Please pass a valid base directory for tang" + return 1 + fi + + [ -z "${TANGD_KEYGEN}" ] && skip_test "WARNING: TANGD_KEYGEN is not defined." + + local db="${basedir}/db" + mkdir -p "${db}" + + if [ -n "${rotate}" ]; then + pushd "${db}" + local k + k=$(find . -name "*.jwk" | wc -l) + if [ "${k}" -gt 0 ]; then + for k in *.jwk; do + mv -f -- "${k}" ".${k}" + done + fi + popd + fi + + "${TANGD_KEYGEN}" "${db}" + + return 0 +} + +# Start a test tang server. +tang_run() { + local basedir="${1}" + local port="${2}" + + if [ -z "${basedir}" ]; then + echo "Please pass a valid base directory for tang" >&2 + return 1 + fi + + if [ -z "${port}" ]; then + echo "Please pass a valid port for tang" >&2 + return 1 + fi + + if ! tang_new_keys "${basedir}"; then + echo "Error creating new keys for tang server" >&2 + return 1 + fi + + local KEYS="${basedir}/db" + + local inetd='--inetd' + [ "${SD_ACTIVATE##*/}" = "systemd-activate" ] && inetd= + + local pid pidfile + pidfile="${basedir}/tang.pid" + + "${SD_ACTIVATE}" ${inetd} -l "${TANG_HOST}":"${port}" \ + -a "${TANGD}" "${KEYS}" & + pid=$! + echo "${pid}" > "${pidfile}" +} + +# Stop tang server. +tang_stop() { + local basedir="${1}" + local pidfile="${basedir}/tang.pid" + [ -f "${pidfile}" ] || return 0 + + local pid + pid=$(<"${pidfile}") + kill "${pid}" +} + +# Wait for the tang server to be operational. +tang_wait_until_ready() { + local port="${1}" + while ! curl --output /dev/null --silent --fail \ + http://"${TANG_HOST}":"${port}"/adv; do + sleep 0.1 + echo -n . >&2 + done +} + +# Get tang advertisement. +tang_get_adv() { + local port="${1}" + local adv="${2}" + + curl -o "${adv}" http://"${TANG_HOST}":"${port}"/adv +} + +export TANG_HOST=127.0.0.1 export DEFAULT_PASS='just-some-test-password-here' diff --git a/src/luks/tests/unlock-tang-luks1 b/src/luks/tests/unlock-tang-luks1 new file mode 100755 index 0000000..841ba01 --- /dev/null +++ b/src/luks/tests/unlock-tang-luks1 @@ -0,0 +1,83 @@ +#!/bin/bash -ex +# vim: set ts=8 shiftwidth=4 softtabstop=4 expandtab smarttab colorcolumn=80: +# +# Copyright (c) 2020 Red Hat, Inc. +# Author: Sergio Correia +# +# 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 . + +TEST=$(basename "${0}") +. tests-common-functions + +. clevis-luks-common-functions + +on_exit() { + [ ! -d "${TMP}" ] && return 0 + tang_stop "${TMP}" + rm -rf "${TMP}" +} + +trap 'on_exit' EXIT +trap 'on_exit' ERR + +TMP="$(mktemp -d)" + +port=$(get_random_port) +tang_run "${TMP}" "${port}" & +tang_wait_until_ready "${port}" + +url="http://${TANG_HOST}:${port}" +adv="${TMP}/adv" +tang_get_adv "${port}" "${adv}" + +cfg=$(printf '{"url":"%s","adv":"%s"}' "$url" "$adv") + +# LUKS1. +DEV="${TMP}/luks1-device" +new_device "luks1" "${DEV}" + +if ! clevis luks bind -f -d "${DEV}" tang "${cfg}" <<< "${DEFAULT_PASS}"; then + error "${TEST}: Bind should have succeeded." +fi + +if ! clevis_luks_unlock_device "${DEV}"; then + error "${TEST}: we were unable to unlock ${DEV}." +fi + +# Let's rotate the tang keys and add another binding with the new key. +tang_new_keys "${TMP}" "rotate-keys" + +# Unlock should still work now. +if ! clevis_luks_unlock_device "${DEV}"; then + error "${TEST}: we should still be able to unlock ${DEV}" +fi + +# Now let's remove the rotated keys. +tang_remove_rotated_keys "${TMP}" + +# Unlock should not work anymore. +if clevis_luks_unlock_device "${DEV}"; then + error "${TEST}: we should not be able to unlock ${DEV}" +fi + +# Now let's add another binding with the new keys. +tang_get_adv "${port}" "${adv}" # Updating the advertisement. +if ! clevis luks bind -f -d "${DEV}" tang "${cfg}" <<< "${DEFAULT_PASS}"; then + error "${TEST}: Bind should have succeeded." +fi + +# Unlock should work again, using the new keys. +if ! clevis_luks_unlock_device "${DEV}"; then + error "${TEST}: we should be able to unlock ${DEV} with the new keys" +fi diff --git a/src/luks/tests/unlock-tang-luks2 b/src/luks/tests/unlock-tang-luks2 new file mode 100755 index 0000000..81822fb --- /dev/null +++ b/src/luks/tests/unlock-tang-luks2 @@ -0,0 +1,83 @@ +#!/bin/bash -ex +# vim: set ts=8 shiftwidth=4 softtabstop=4 expandtab smarttab colorcolumn=80: +# +# Copyright (c) 2020 Red Hat, Inc. +# Author: Sergio Correia +# +# 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 . + +TEST=$(basename "${0}") +. tests-common-functions + +. clevis-luks-common-functions + +on_exit() { + [ ! -d "${TMP}" ] && return 0 + tang_stop "${TMP}" + rm -rf "${TMP}" +} + +trap 'on_exit' EXIT +trap 'on_exit' ERR + +TMP="$(mktemp -d)" + +port=$(get_random_port) +tang_run "${TMP}" "${port}" & +tang_wait_until_ready "${port}" + +url="http://${TANG_HOST}:${port}" +adv="${TMP}/adv" +tang_get_adv "${port}" "${adv}" + +cfg=$(printf '{"url":"%s","adv":"%s"}' "$url" "$adv") + +# LUKS2. +DEV="${TMP}/luks2-device" +new_device "luks2" "${DEV}" + +if ! clevis luks bind -f -d "${DEV}" tang "${cfg}" <<< "${DEFAULT_PASS}"; then + error "${TEST}: Bind should have succeeded." +fi + +if ! clevis_luks_unlock_device "${DEV}"; then + error "${TEST}: we were unable to unlock ${DEV}." +fi + +# Let's rotate the tang keys and add another binding with the new key. +tang_new_keys "${TMP}" "rotate-keys" + +# Unlock should still work now. +if ! clevis_luks_unlock_device "${DEV}"; then + error "${TEST}: we should still be able to unlock ${DEV}" +fi + +# Now let's remove the rotated keys. +tang_remove_rotated_keys "${TMP}" + +# Unlock should not work anymore. +if clevis_luks_unlock_device "${DEV}"; then + error "${TEST}: we should not be able to unlock ${DEV}" +fi + +# Now let's add another binding with the new keys. +tang_get_adv "${port}" "${adv}" # Updating the advertisement. +if ! clevis luks bind -f -d "${DEV}" tang "${cfg}" <<< "${DEFAULT_PASS}"; then + error "${TEST}: Bind should have succeeded." +fi + +# Unlock should work again, using the new keys. +if ! clevis_luks_unlock_device "${DEV}"; then + error "${TEST}: we should be able to unlock ${DEV} with the new keys" +fi -- 2.18.4