I’m impressed with Yubikeys and have tinkered a bit with setting up a Linux workstation to use them. The pièce de résistance is requiring two of three Yubikeys to decrypt a system disk. Adding the Yubikey as a second factor to authentication and lock user sessions when a Yubikey is removed is also useful.
Majority unlock for LUKS-encrypted disks
LUKS allows us to have up to 8 passwords for a single disk. Let’s think of how we might use this to allow any two users from a set of three to start the computer by unlocking the encrypted drive. We have three people: A, B and C. They have a password each:
- A => passwordA
- B => passwordB
- C => passwordC
To require two users to unlock the disk we must make every password a combination of two passwords:
- Slot 1 => passwordApasswordB
- Slot 2 => passwordApasswordC
- Slot 3 => passwordBpasswordC
The users would here have to remember their internal ordering since we don’t have a slot with passwordCpasswordB for instance. LUKS will try a provided password against all slots so if two users enter the password(in the right order) the disk will be unlocked. Now we add Yubikeys to the mix which helps us a bit since that allows the scripts to order things based on the serials of each user’s Yubikey. I’ve swiped a lot of code from this suite of tools: https://github.com/cornelinux/yubikey-luks
Indeed I just installed it, replaced /usr/share/yubikey-luks/ykluks-keyscript and made my own enroll-script so this is little more than a hack layered on top of yubikey-luks.
To enroll three Yubikeys we first have to enable challenge-response mode which is basically just “encrypt incoming data with a secret key and return the output”.
ykpersonalize -2 -ochal-resp -ochal-hmac -ohmac-lt64 -oserial-api-visible
To then enroll:
#!/bin/bash
if [ -z $1 ]; then
echo "You must provide a disk name as an argument. Example: /dev/sda3." 1>&2
exit 1
fi
DISK=$1
if [ "$(id -u)" -ne 0 ]; then
echo "You must be root." 1>&2
exit 1
fi
declare -a STEPS=(0 1 2)
declare -a KEYSERIAL
declare -a KEYPASS
for INDX in ${STEPS[@]};
do
echo "Index: $INDX"
COUNTER=10
while [ "$COUNTER" -gt 1 ];
do
sleep 0.5
COUNTER=$((COUNTER-1))
if ykinfo -n$INDX -q -2;
then
echo "Success!"
break
else
echo "No YubiKey found."
fi
done
SERIAL=$(ykinfo -s -n$INDX | cut -d ' ' -f2)
echo "Enter challenge(password) for key $SERIAL: "
read -s PW
KEYSERIAL[$INDX]="$SERIAL"
KEYPASS[$SERIAL]=$(printf %s "$PW" | ykchalresp -n$INDX -2 -i- 2>/dev/null || true)
done
SORTEDSERIALS=$(for K in ${!KEYPASS[@]};
do
echo $K;
done | sort)
declare -a PASSWORDS
INDX=0
for K in $SORTEDSERIALS;
do
echo "$K => ${KEYPASS[$K]}";
PASSWORDS[$INDX]=${KEYPASS[$K]};
INDX=$((INDX+1))
done
declare -a COMBINATIONS
COMBINATIONS[0]="${PASSWORDS[0]}${PASSWORDS[1]}"
COMBINATIONS[1]="${PASSWORDS[0]}${PASSWORDS[2]}"
COMBINATIONS[2]="${PASSWORDS[1]}${PASSWORDS[2]}"
OLD=$(/lib/cryptsetup/askpass "Please provide an existing passphrase. This is NOT the passphrase you just entered, this is the passphrase that you currently use to unlock your LUKS encrypted drive:")
SLOTS=$(seq 1 3)
for SLOT in $SLOTS;
do
printf '%s\n' "$OLD" "${COMBINATIONS[$SLOT-1]}" "${COMBINATIONS[$SLOT-1]}" | cryptsetup --key-slot="$SLOT" luksAddKey "$DISK" 2>&1;
SLOT=$((SLOT+1))
done
This is for a total of 3 users with their own keys. It could be made to handle 4 but 5 keys leaves too many permutations to fit into 8 slots. Execution looks like this:
root@yktest2:~# ./ykluks-enroll.sh /dev/sda3
Index: 0
1
Success!
Enter challenge(password) for key 24130422: [password entered]
Index: 1
1
Success!
Enter challenge(password) for key 19652688: [password entered]
Index: 2
1
Success!
Enter challenge(password) for key 23882290: [password entered]
Now we need to introduce a key script /usr/share/yubikey-luks/ykluks-keyscript:
#!/bin/bash
#
#
message()
{
if [ -x /bin/plymouth ] && plymouth --ping; then
plymouth message --text="$*"
else
echo "$@" >&2
fi
return 0
}
# source for log_*_msg() functions, see LP: #272301
if [ -e /scripts/functions ] ; then
. /scripts/functions
else
. /usr/share/initramfs-tools/scripts/functions
fi
if [ -z "$cryptkeyscript" ]; then
cryptkey="Unlocking the disk $cryptsource ($crypttarget)\\nEnter passphrase: "
if [ -x /bin/plymouth ] && plymouth --ping; then
cryptkeyscript="plymouth ask-for-password --prompt" cryptkey=$(printf '%s' "$cryptkey")
else
cryptkeyscript="/lib/cryptsetup/askpass"
fi
fi
check_yubikey_present="$(ykinfo -n0 -q -2)"
check_yubikey2_present="$(ykinfo -n1 -q -2)"
if [ "$check_yubikey_present" = "1" ]; then
N0=$(ykinfo -n0 -s | cut -d ' ' -f 2)
if [ "$check_yubikey2_present" = "1" ]; then
N1=$(ykinfo -n1 -s | cut -d ' ' -f 2)
if [ "$N0" -lt "$N1" ];
then
declare -a ORDER=(0 1)
else
declare -a ORDER=(1 0)
fi
else
PW="$($cryptkeyscript "Please enter disk password: ")"
printf '%s' "$PW"
fi
else
PW="$($cryptkeyscript "Please enter disk password: ")"
printf '%s' "$PW"
fi
FINALPW=""
for INDX in "${ORDER[@]}";
do
SERIAL=$(ykinfo -n$INDX -s)
PW="$($cryptkeyscript "Please enter challenge for YubiKey $SERIAL: ")"
R="$(printf %s "$PW" | ykchalresp -n$INDX -2 -i- 2>/dev/null || true)"
message "Retrieved the response from Yubikey $SERIAL"
FINALPW="$FINALPW$R"
done
printf '%s' "$FINALPW"
exit 0
Oh, right! I had to add bash to initramfs since my use of arrays isn’t compatible with dash which is what Ubuntu typically includes. So add this to /usr/share/initramfs-tools/hooks/yubikey-luks:
cp /usr/bin/bash "${DESTDIR}/bin/bash"
Then run update-initramfs -u
Yubikey U2F on authentication
Install libpam-u2f and pamu2fcfg packages:
apt install libpam-u2f pamu2fcfg
Then add this line to the bottom of /etc/pam.d/common-auth:
auth required pam_u2f.so authfile=/etc/u2f_mappings cue
Include nouserok to allow users without a Yubikey to log in, i.e. only apply the requirement for those users included in /etc/u2f_mappings: https://developers.yubico.com/pam-u2f/
Which bring us to adding U2F signatures to authfile. The user can run this:
cjp@yktest2:~$ pamu2fcfg
Enter PIN for /dev/hidraw2:
cjp:77hsMUYzPD0poXbu51/TWGW6roJ31F35G01JoiEskczwxqOvzb5zTgLsnWWo2nO0MmZ6L7erxJz2DufhQDuCs9GEQ==,Wrg4zmgQedALIQCBYTAxoIq/bd/Se2tqtOvVn6JdQmezN05Gt3qLmFGvMA7iXV6u2OHN/mQosg/46/LyIoY9gnow==,es256,+presence
And then the admin can add the generated line to /etc/u2f_mappings.
Lock on Yubikey removal
SUBSYSTEM=="usb", ACTION=="remove", RUN+="/usr/bin/loginctl lock-sessions"
I can’t get it to trigger based on ATTRS which seems reasonable since the device is disconnected when the rule is run. Also loginctl lock-sessions only work for some display managers but works for gdm3+gnome.