Skip to content

Commit

Permalink
fido2luks: unlock LUKS volumes at boot time using a FIDO2 token
Browse files Browse the repository at this point in the history
  • Loading branch information
bertogg committed May 13, 2024
0 parents commit f8d55b8
Show file tree
Hide file tree
Showing 15 changed files with 643 additions and 0 deletions.
339 changes: 339 additions & 0 deletions LICENSE

Large diffs are not rendered by default.

9 changes: 9 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
build:
@echo "Run 'make install' to install fido2luks"

clean:
@echo "Nothing to do"

install:
install -D -m 0755 keyscript.sh $(DESTDIR)/lib/fido2luks/keyscript.sh
install -D -m 0755 initramfs-hook $(DESTDIR)/etc/initramfs-tools/hooks/fido2luks
72 changes: 72 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
# fido2luks

This is an extension to initramfs-tools to unlock LUKS-encrypted
volumes at boot time using a FIDO2 token (YubiKey, Nitrokey, ...).

`fido2luks` is designed for scenarios where a FIDO2 token was enrolled
into a LUKS volume using `systemd-cryptenroll --fido2-device` but
systemd itself is not used in the initramfs.

This has successfully been tested with Debian bookworm and trixie (as
of May 2024).

## How to use it

- First of all, a word of warning: this can potentially render your
system unbootable, so make sure that you have a backup of your files
or a working initramfs that you can use as a fallback in case things
go wrong.

- Dependencies: you need `initramfs-tools`, `fido2-tools` and `jq` on
your system.

- Install `fido2luks`: you can generate a Debian package using the
scripts that are included for convenience. Simply run `fakeroot
debian/rules binary` and install the resulting `.deb` file. If you
prefer not to do that you can run `make install` instead.

- Make sure that the LUKS volume has been set up, e.g.:
`systemd-cryptenroll --fido2-device=auto --fido2-with-client-pin=true --fido2-with-user-presence=true /dev/XXX`.
You should be able to see the `systemd-fido2` token data if you run
`cryptsetup luksDump /dev/XXX`.

- Edit `/etc/crypttab` and add `keyscript=/lib/fido2luks/keyscript.sh`
to the options of the volume that you want to unlock.

- Generate a new initramfs with `update-initramfs -u`.

This should be all. Next time you boot the system `fido2luks` should
detect if your FIDO2 token is inserted and use it to unlock the LUKS
volume. If the token is not detected then it will fall back to using a
regular passphrase as usual.

## How this works

If you are not interested in the technical details you can skip this
section.

When systemd enrolls a FIDO2 token into a LUKS volume it uses an
extension called hmac-secret, supported by many hardware tokens.

In a nutshell, the token calculates an HMAC using a secret that never
leaves the device and a salt provided by the user. The result is sent
back to the user and is used to unlock the LUKS volume.

Since nothing is stored on the hardware token itself the user needs to
provide some data that is kept on the LUKS header:

- A credential ID (previously generated during the enrollment process).
- A _relying party_ ID (`io.systemd.cryptsetup` in this case).
- The aforementioned salt (which should be random and different for
each LUKS volume).
- Some settings such as whether to require a PIN or presence
verification (usually physically touching the USB key).

You can look at the scripts under the examples/ directory to see how
to generate your own credentials and secrets. See also the
`fido2-cred(1)` and `fido2-assert(1)` manpages for more details.

## Credits and license

fido2luks was written by Alberto Garcia and is distributed under the
GNU GPL.
5 changes: 5 additions & 0 deletions debian/changelog
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
fido2luks (0.0.1-1) UNRELEASED; urgency=medium

* Initial release.

-- Alberto Garcia <[email protected]> Sat, 11 May 2024 18:07:00 +0200
16 changes: 16 additions & 0 deletions debian/control
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
Source: fido2luks
Section: admin
Priority: optional
Maintainer: Alberto Garcia <[email protected]>
Build-Depends: debhelper-compat (= 13)
Standards-Version: 4.7.0

Package: fido2luks
Architecture: all
Depends: ${shlibs:Depends}, ${misc:Depends}, initramfs-tools, fido2-tools, jq
Description: Unlock a LUKS volume using a FIDO2 token on boot
This is an extension to initramfs-tools to unlock a LUKS-encrypted
disk at boot time using a FIDO2 token.
.
It is designed for scenarios where a FIDO2 token was enrolled into a
LUKS volume using systemd-cryptenroll --fido2-device.
21 changes: 21 additions & 0 deletions debian/copyright
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
Upstream-Name: fido2luks

Files: *
Copyright: 2024 Alberto Garcia <[email protected]>
License: GPL-2+
This package 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 2 of the License, or
(at your option) any later version.
.
This package 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 <https://www.gnu.org/licenses/>
.
On Debian systems, the complete text of the GNU General
Public License version 2 can be found in "/usr/share/common-licenses/GPL-2".
1 change: 1 addition & 0 deletions debian/docs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
README.md
1 change: 1 addition & 0 deletions debian/examples
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
examples/*
3 changes: 3 additions & 0 deletions debian/rules
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
#!/usr/bin/make -f
%:
dh $@
1 change: 1 addition & 0 deletions debian/source/format
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
3.0 (quilt)
4 changes: 4 additions & 0 deletions examples/crypttab
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# In order to use fido2luks add 'keyscript=/lib/fido2luks/keyscript.sh'
# and run update-initramfs -u

<device>_crypt UUID=<LUKS-VOLUME-UUID> none luks,keyscript=/lib/fido2luks/keyscript.sh
36 changes: 36 additions & 0 deletions examples/generate-cred.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
#!/bin/sh

set -eu

RELYING_PARTY_ID="org.test.some_app"
USER_NAME="User Name"
USER_ID="User ID"
CLIENT_DATA="" # This can be empty

FIDO2_DEVICE=$(fido2-token -L | head -n 1 | cut -d : -f 1)

if [ -z "$FIDO2_DEVICE" ]; then
echo "ERROR: no FIDO2 device found"
exit 1
fi

CRED_PARAMS="$(mktemp /tmp/cred-params.XXXXXX)"
CRED_DATA="$(mktemp /tmp/cred-data.XXXXXX)"
CRED_VERIFY="$(mktemp /tmp/cred-verify.XXXXXX)"
trap "rm -f $CRED_PARAMS $CRED_DATA $CRED_VERIFY" INT EXIT

printf "%s\n%s\n%s\n%s\n" \
$(echo -n "$CLIENT_DATA" | openssl sha256 -binary | base64) \
"$RELYING_PARTY_ID" \
"$USER_NAME" \
$(echo -n "$USER_ID" | openssl sha256 -binary | base64) > "$CRED_PARAMS"

fido2-cred -M -h -i "$CRED_PARAMS" -o "$CRED_DATA" "$FIDO2_DEVICE"
fido2-cred -V -h -i "$CRED_DATA" -o "$CRED_VERIFY"

CRED_ID=$(head -n 1 "$CRED_VERIFY")

echo "A new credential has been generated"
echo "ID: $CRED_ID"
echo "Public key:"
tail -n +2 "$CRED_VERIFY"
50 changes: 50 additions & 0 deletions examples/generate-hmac-secret.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
#!/bin/bash

set -eu

# Set CREDENTIAL_ID and PUBLIC_KEY to the values that you got
# from generate-cred.sh, otherwise this won't work

RELYING_PARTY_ID="org.test.some_app"
CREDENTIAL_ID="E2NDqozxIGOlcUhlrg6+XIjZSRC8i5C69PgOiHzWXGBZTJ6No9fa6fEcPvQjxL5slKGVr7ioYBcxKwPREJnuMA=="
SALT="The HMAC secret depends on this value"
CLIENT_DATA="" # This can be empty

# Set these to true or false
REQUIRE_PIN=false
REQUIRE_USER_PRESENCE=true

# The public key is only needed to verify the result, see below
PUBLIC_KEY='
-----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEjX1Eiv/1H39f+b+MmSTymbdR8l3+
GqJHf3X0CyREljyHi7mS5LgkmsyvO0fgc6SryYCUKG6MREdnKirNilXLYQ==
-----END PUBLIC KEY-----
'

FIDO2_DEVICE=$(fido2-token -L | head -n 1 | cut -d : -f 1)

if [ -z "$FIDO2_DEVICE" ]; then
echo "ERROR: no FIDO2 device found"
exit 1
fi

ASSERT_PARAMS="$(mktemp /tmp/cred-params.XXXXXX)"
ASSERT_DATA="$(mktemp /tmp/cred-data.XXXXXX)"
trap "rm -f $ASSERT_PARAMS $ASSERT_DATA" INT EXIT

printf "%s\n%s\n%s\n%s\n" \
$(echo -n "$CLIENT_DATA" | openssl sha256 -binary | base64) \
"$RELYING_PARTY_ID" \
"$CREDENTIAL_ID" \
$(echo -n "$SALT" | openssl sha256 -binary | base64) > "$ASSERT_PARAMS"

fido2-assert -G -h \
-t up="$REQUIRE_USER_PRESENCE" -t pin="$REQUIRE_PIN" \
-i "$ASSERT_PARAMS" -o "$ASSERT_DATA" "$FIDO2_DEVICE"

# If you want to verify the result with the public key:
# fido2-assert -V -h -i "$ASSERT_DATA" <(echo "$PUBLIC_KEY") es256

HMAC_SECRET="$(tail -n 1 "$ASSERT_DATA")"
echo "The generated secret is $HMAC_SECRET"
21 changes: 21 additions & 0 deletions initramfs-hook
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
#!/bin/sh

PREREQ=""

prereqs() {
echo "$PREREQ"
}

case "$1" in
prereqs)
prereqs
exit 0
;;
esac

. "${CONFDIR}/initramfs.conf"
. /usr/share/initramfs-tools/hook-functions

copy_exec /usr/bin/fido2-assert /bin
copy_exec /usr/bin/fido2-token /bin
copy_exec /usr/bin/jq /bin
64 changes: 64 additions & 0 deletions keyscript.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
#!/bin/sh

cleanup () {
rm -f "$ASSERT_PARAMS" "$LUKS_TOKEN"
}

ASSERT_PARAMS=$(mktemp -t params.XXXXXX)
LUKS_TOKEN=$(mktemp -t token.XXXXXX)
trap cleanup INT EXIT

cryptsetup luksDump --dump-json-metadata "$CRYPTTAB_SOURCE" | \
jq -e '[.tokens[] | select(."fido2-credential" != null)][0]' > "$LUKS_TOKEN"

if [ $? -ne 0 ]; then
echo "*** No FIDO2 credentials found in $CRYPTTAB_SOURCE" >&2
else
echo "*** Waiting for a FIDO2 authenticator..." >&2
for f in $(seq 5); do
FIDO2_AUTHENTICATOR=$(fido2-token -L)
[ -n "$FIDO2_AUTHENTICATOR" ] && break
sleep 1
done

if [ -n "$FIDO2_AUTHENTICATOR" ]; then
echo "*** Found FIDO2 authenticator $FIDO2_AUTHENTICATOR" >&2

REQ_PIN=$(jq -r '."fido2-clientPin-required"' "$LUKS_TOKEN")
REQ_UP=$(jq -r '."fido2-up-required"' "$LUKS_TOKEN")

# echo -n | openssl sha256 -binary | base64
jq -r '"47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=",
."fido2-rp",
."fido2-credential",
."fido2-salt"' "$LUKS_TOKEN" > "$ASSERT_PARAMS"

if [ "$REQ_PIN" = "true" ]; then
stty -echo
fi

sleep 2

FIDO2_DEV=${FIDO2_AUTHENTICATOR%%:*}
SECRET=$(fido2-assert -G -h -t up="$REQ_UP" -t pin="$REQ_PIN" \
-i "$ASSERT_PARAMS" "$FIDO2_DEV" | tail -n 1)

if [ "$REQ_PIN" = "true" ]; then
stty echo
fi

if [ -n "$SECRET" ]; then
echo >&2
echo -n "$SECRET"
exit 0
else
echo "*** Error obtaining secret from $FIDO2_DEV" >&2
fi
else
echo "*** No FIDO2 authenticator found" >&2
fi
fi

echo "*** Unlocking $CRYPTTAB_NAME using a regular passphrase" >&2

/lib/cryptsetup/askpass "Enter passphrase: "

0 comments on commit f8d55b8

Please sign in to comment.