Rewrite as a pre-boot authentication module (mostly) comforming to the design specification of

'YubiKey Integration for Full Disk Encryption Pre-Boot Authentication (Copyright) Yubico, 2011 Version: 1.1'.

Used binaries:
  * uuidgen - for generation of random sequence numbers
  * ykchalresp - for challenging a Yubikey
  * ykinfo - to check if a Yubikey is plugged in at boot (fallback to passphrase authentication otherwise)
  * openssl - for calculation of SHA-1, HMAC-SHA-1, as well as AES-256-CTR (de/en)cryption

Main differences to the specification mentioned above:
  * No user management (yet), only one password+yubikey per LUKS device
  * SHA-512 instead of CRC-16 for checksum

Main differences to the previous implementation:
  * Instead of changing the key slot of the LUKS device each boot,
    the actual key for the LUKS device will be encrypted itself
  * Since the response for the new challenge is now calculated
    locally with openssl, the MITM-USB-attack with which previously
    an attacker could obtain the new response (that was used as the new
    encryption key for the LUKS device) by listening to the
    Yubikey has ideally become useless (as long as uuidgen can
    successfuly generate new random sequence numbers).

Remarks:
  * This is not downwards compatible to the previous implementation
This commit is contained in:
Moritz Maxeiner 2014-01-28 04:02:51 +01:00
parent 333f5caaf9
commit 407a770161

View file

@ -31,140 +31,148 @@ let
fi
''}
${optionalString (luks.yubikeySupport && (yubikey != null)) ''
mkdir -p ${yubikey.challenge.mountPoint}
mount -t ${yubikey.challenge.fsType} ${toString yubikey.challenge.device} ${yubikey.challenge.mountPoint}
response="$(ykchalresp -${toString yubikey.yubikeySlot} "`cat ${yubikey.challenge.mountPoint}${yubikey.challenge.file}`" 2>/dev/null || true)"
if [ -z "$response" ]; then
echo -n "waiting 10 seconds for yubikey to appear..."
for try in $(seq 10); do
sleep 1
response="$(ykchalresp -${toString yubikey.yubikeySlot} "`cat ${yubikey.challenge.mountPoint}${yubikey.challenge.file}`" 2>/dev/null || true)"
if [ ! -z "$response" ]; then break; fi
echo -n .
done
echo "ok"
fi
${optionalString yubikey.twoFactor ''
if [ ! -z "$response" ]; then
echo -n "Enter two-factor passphrase: "
read -s passphrase
current_key="$passphrase$response"
fi
''}
${optionalString (!yubikey.twoFactor) ''
if [ ! -z "$response" ]; then
current_key="$response"
fi
''}
''}
# open luksRoot and scan for logical volumes
${optionalString ((!luks.yubikeySupport) || (yubikey == null)) ''
open_normally() {
cryptsetup luksOpen ${device} ${name} ${optionalString allowDiscards "--allow-discards"} \
${optionalString (keyFile != null) "--key-file=${keyFile} ${optionalString (keyFileSize != null) "--keyfile-size=${toString keyFileSize}"}"}
''}
}
${optionalString (luks.yubikeySupport && (yubikey != null)) ''
if [ -z "$response" ]; then
cryptsetup luksOpen ${device} ${name} ${optionalString allowDiscards "--allow-discards"} \
${optionalString (keyFile != null) "--key-file=${keyFile} ${optionalString (keyFileSize != null) "--keyfile-size=${toString keyFileSize}"}"}
rbtohex() {
od -An -vtx1 | tr -d ' \n'
}
hextorb() {
tr '[:lower:]' '[:upper:]' | sed -e 's|\([0-9A-F]\{2\}\)|\\\\\\x\1|gI' | xargs printf
}
take() {
local c="$1"
shift
head -c $c "$@"
}
drop() {
local c=$1
shift
if [ -e "$1" ]; then
cat "$1" | ( dd of=/dev/null bs="$c" count=1 2>/dev/null ; dd 2>/dev/null )
else
echo $current_key | cryptsetup luksOpen ${device} ${name} ${optionalString allowDiscards "--allow-discards"} --key-file=-
( dd of=/dev/null bs="$c" count=1 2>/dev/null ; dd 2>/dev/null )
fi
}
open_yubikey() {
mkdir -p ${yubikey.storage.mountPoint}
mount -t ${yubikey.storage.fsType} ${toString yubikey.storage.device} ${yubikey.storage.mountPoint}
local uuid_r
uuid_r="$(take 16 ${yubikey.storage.mountPoint}${yubikey.storage.path} | rbtohex)"
local uuid_luks
uuid_luks="$(cryptsetup luksUUID ${device} | take 36 | tr -d '-')"
local k_user
local challenge
local k_blob
local aes_blob_decrypted
local checksum_correct
local checksum
if [ $? != "0" ]; then
for try in $(seq 3); do
${optionalString (!yubikey.twoFactor) ''
sleep 1
''}
${optionalString yubikey.twoFactor ''
echo -n "Enter two-factor passphrase: "
read -s passphrase
current_key="$passphrase$response"
read -s k_user
''}
echo $current_key | cryptsetup luksOpen ${device} ${name} ${optionalString allowDiscards "--allow-discards"} --key-file=-
if [ $? == "0" ]; then break; fi
echo -n .
challenge="$(echo -n $k_user$uuid_r$uuid_luks | openssl-wrap dgst -binary -sha1 | rbtohex)"
k_blob="$(ykchalresp -${toString yubikey.slot} -x $challenge 2>/dev/null)"
aes_blob_decrypted="$(drop 16 ${yubikey.storage.mountPoint}${yubikey.storage.path} | openssl-wrap enc -d -aes-256-ctr -K $k_blob -iv $uuid_r | rbtohex)"
checksum="$(echo -n $aes_blob_decrypted | hextorb | drop 84 | rbtohex)"
if [ "$(echo -n $aes_blob_decrypted | hextorb | take 84 | openssl-wrap dgst -binary -sha512 | rbtohex)" == "$checksum" ]; then
checksum_correct=1
break
else
checksum_correct=0
echo "Authentication failed!"
fi
done
if [ "$checksum_correct" != "1" ]; then
umount ${yubikey.storage.mountPoint}
echo "Maximum authentication errors reached"
exit 1
fi
mkdir -p ${yubikey.ramfsMountPoint}
# A ramfs is used here to ensure that the file used to update
# the key slot with cryptsetup will never get swapped out.
# Warning: Do NOT replace with tmpfs!
mount -t ramfs none ${yubikey.ramfsMountPoint}
local k_yubi
k_yubi="$(echo -n $aes_blob_decrypted | hextorb | take 20 | rbtohex)"
local k_luks
k_luks="$(echo -n $aes_blob_decrypted | hextorb | drop 20 | take 64 | rbtohex)"
echo -n "$k_luks" | hextorb | cryptsetup luksOpen ${device} ${name} ${optionalString allowDiscards "--allow-discards"} --key-file=-
update_failed=false
old_challenge=$(cat ${yubikey.challenge.mountPoint}${yubikey.challenge.file})
new_challenge=$(uuidgen)
local new_uuid_r
new_uuid_r="$(uuidgen)"
if [ $? != "0" ]; then
for try in $(seq 10); do
sleep 1
new_challenge=$(uuidgen)
new_uuid_r="$(uuidgen)"
if [ $? == "0" ]; then break; fi
if [ $try -eq 10 ]; then update_failed=true; fi
done
fi
if [ "$update_failed" == false ]; then
echo $new_challenge > ${yubikey.challenge.mountPoint}${yubikey.challenge.file}
response="$(ykchalresp -${toString yubikey.yubikeySlot} "`cat ${yubikey.challenge.mountPoint}${yubikey.challenge.file}`" 2>/dev/null || true)"
if [ -z "$response" ]; then
new_uuid_r="$(echo -n $new_uuid_r | take 36 | tr -d '-')"
local new_challenge
new_challenge="$(echo -n $k_user$new_uuid_r$uuid_luks | openssl-wrap dgst -binary -sha1 | rbtohex)"
local new_k_blob
new_k_blob="$(echo -n $new_challenge | hextorb | openssl-wrap dgst -binary -sha1 -mac HMAC -macopt hexkey:$k_yubi | rbtohex)"
echo -n "$new_uuid_r" | hextorb > ${yubikey.storage.mountPoint}${yubikey.storage.path}
echo -n "$k_yubi$k_luks$checksum" | hextorb | openssl-wrap enc -e -aes-256-ctr -K "$new_k_blob" -iv "$new_uuid_r" >> ${yubikey.storage.mountPoint}${yubikey.storage.path}
else
echo "Warning: Could not obtain new UUID, current challenge persists!"
fi
umount ${yubikey.storage.mountPoint}
}
ykinfo -v
yubikey_missing="$(ykinfo -v 1>/dev/null 2>&1)$?"
if [ "$yubikey_missing" != "0" ]; then
echo -n "waiting 10 seconds for yubikey to appear..."
for try in $(seq 10); do
sleep 1
response="$(ykchalresp -${toString yubikey.yubikeySlot} "`cat ${yubikey.challenge.mountPoint}${yubikey.challenge.file}`" 2>/dev/null || true)"
if [ ! -z "$response" ]; then break; fi
ykinfo -v
yubikey_missing="$(ykinfo -v 1>/dev/null 2>&1)$?"
if [ "$yubikey_missing" == "0" ]; then break; fi
echo -n .
done
echo "ok";
echo "ok"
fi
if [ ! -z "$response" ]; then
${optionalString yubikey.twoFactor ''
new_key="$passphrase$response"
if [ "$yubikey_missing" != "0" ]; then
echo "no yubikey found, falling back to non-yubikey open procedure"
open_normally
else
open_yubikey
fi
''}
${optionalString (!yubikey.twoFactor) ''
new_key="$response"
''}
echo $new_key > ${yubikey.ramfsMountPoint}/new_key
echo $current_key | cryptsetup luksChangeKey ${device} --key-file=- --key-slot ${toString yubikey.luksKeySlot} ${yubikey.ramfsMountPoint}/new_key
if [ $? != "0" ]; then
for try in $(seq 10); do
sleep 1
echo $current_key | cryptsetup luksChangeKey ${device} --key-file=- --key-slot ${toString yubikey.luksKeySlot} ${yubikey.ramfsMountPoint}/new_key
if [ $? == "0" ]; then break; fi
if [ $try -eq 10 ]; then update_failed=true; fi
done
fi
rm -f ${yubikey.ramfsMountPoint}/new_key
if [ "$update_failed" == true ]; then
echo $old_challenge > ${yubikey.challenge.mountPoint}${yubikey.challenge.file}
echo "Warning: Could not update luks header with new key for ${device}, old challenge restored!"
fi
else
echo $old_challenge > ${yubikey.challenge.mountPoint}${yubikey.challenge.file}
echo "Warning: No yubikey present to challenge for ${device}, old challenge restored!"
fi
else
echo "Warning: New challenge could not be obtained for ${device}, old challenge persists!"
fi
umount ${yubikey.ramfsMountPoint}
umount ${yubikey.challenge.mountPoint}
fi
# open luksRoot and scan for logical volumes
${optionalString ((!luks.yubikeySupport) || (yubikey == null)) ''
open_normally
''}
'';
@ -283,19 +291,13 @@ in
description = "TODO";
};
yubikeySlot = mkOption {
slot = mkOption {
default = 2;
type = types.int;
description = "TODO";
};
luksKeySlot = mkOption {
default = 1;
type = types.int;
description = "TODO";
};
challenge = mkOption {
storage = mkOption {
type = types.optionSet;
description = "TODO";
@ -313,24 +315,18 @@ in
};
mountPoint = mkOption {
default = "/crypt-challenge";
default = "/crypt-storage";
type = types.string;
description = "TODO";
};
file = mkOption {
default = "/crypt-challenge";
path = mkOption {
default = "/crypt-storage/default";
type = types.string;
description = "TODO";
};
};
};
ramfsMountPoint = mkOption {
default = "/crypt-update";
type = types.string;
description = "TODO";
};
};
};
@ -361,6 +357,7 @@ in
cp -pdvn $lib $out/lib
cp -pvn $(readlink -f $lib) $out/lib
done
${optionalString luks.yubikeySupport ''
cp -pdv ${pkgs.utillinux}/bin/uuidgen $out/bin
for lib in $(ldd $out/bin/uuidgen |grep '=>' |grep /nix/store/ |cut -d' ' -f3); do
@ -373,6 +370,26 @@ in
cp -pdvn $lib $out/lib
cp -pvn $(readlink -f $lib) $out/lib
done
cp -pdv ${pkgs.ykpers}/bin/ykinfo $out/bin
for lib in $(ldd $out/bin/ykinfo |grep '=>' |grep /nix/store/ |cut -d' ' -f3); do
cp -pdvn $lib $out/lib
cp -pvn $(readlink -f $lib) $out/lib
done
cp -pdv ${pkgs.openssl}/bin/openssl $out/bin
for lib in $(ldd $out/bin/openssl |grep '=>' |grep /nix/store/ |cut -d' ' -f3); do
cp -pdvn $lib $out/lib
cp -pvn $(readlink -f $lib) $out/lib
done
mkdir -p $out/etc/ssl
cp -pdv ${pkgs.openssl}/etc/ssl/openssl.cnf $out/etc/ssl
cat > $out/bin/openssl-wrap <<EOF
#!$out/bin/sh
EOF
chmod +x $out/bin/openssl-wrap
''}
'';
@ -381,6 +398,13 @@ in
${optionalString luks.yubikeySupport ''
$out/bin/uuidgen --version
$out/bin/ykchalresp -V
$out/bin/ykinfo -V
cat > $out/bin/openssl-wrap <<EOF
#!$out/bin/sh
export OPENSSL_CONF=$out/etc/ssl/openssl.cnf
$out/bin/openssl "\$@"
EOF
$out/bin/openssl-wrap version
''}
'';