Linux Workstation with Yubikey Protection

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.